--- name: ably-realtime description: Professional guide to Ably Realtime for building real-time React/TypeScript applications with pub-sub messaging, channels, presence tracking, Spaces (collaborative UI), LiveObjects (shared state), Chat SDK (complete messaging), and LiveSync (database synchronization). Use when working with Ably, real-time messaging, WebSockets, pub-sub, channels, presence, collaborative features, live cursors, avatar stacks, component locking, shared state, conflict-free updates, chat rooms, typing indicators, message reactions, database sync, outbox pattern, useChannel, usePresence, useSpace, useMembers, useCursors, useMessages, useTyping, LiveCounter, LiveMap, or integrating Ably with Neon/PostgreSQL for persistent real-time data. --- # Ably Realtime for React/TypeScript Ably Realtime is a platform for building scalable real-time applications with pub-sub messaging, presence tracking, collaborative features, chat, and database synchronization. ## When to Use Each Feature Ably provides different abstractions for different real-time use cases: - **Channels (Core Pub-Sub)**: Custom real-time messaging, notifications, live updates, event broadcasting - **Spaces**: Participant state in collaborative UIs (live cursors, avatar stacks, user locations, component locking) - **LiveObjects**: Application state synchronization (counters, voting, shared configurations, game state) with conflict-free updates - **Chat SDK**: Complete messaging apps (1:1 chat, group conversations, livestream chat, support tickets) - **LiveSync**: Database-to-client synchronization (broadcasting Postgres changes, outbox pattern, transactional consistency) ## Installation ```bash # Core Ably (required) npm install ably # Additional packages (install as needed) npm install @ably/spaces # For Spaces npm install @ably/chat # For Chat SDK npm install @ably-labs/models # For LiveSync Models SDK ``` ## Basic Setup All Ably features require a Realtime client. Create the client outside React components to prevent reconnections on re-renders: ```typescript // main.tsx or app.tsx import * as Ably from 'ably'; import { AblyProvider } from 'ably/react'; // Create client OUTSIDE components const realtimeClient = new Ably.Realtime({ key: import.meta.env.VITE_ABLY_API_KEY, clientId: 'unique-user-id', // Required for Spaces and Chat }); function Root() { return ( ); } ``` For production applications, use token authentication instead of API keys. See [references/auth-security.md](references/auth-security.md). ## Quick Start: Channels (Core Pub-Sub) Basic real-time messaging with channels: ```typescript import { ChannelProvider, useChannel } from 'ably/react'; // Wrap with ChannelProvider // Use in component function NotificationComponent() { const { publish } = useChannel('notifications', (message) => { console.log('Received:', message.data); // Update local state with message }); const sendNotification = () => { publish('alert', { text: 'New update!', timestamp: Date.now() }); }; return ; } ``` For detailed channel operations, presence tracking, and history, see [references/channels/](references/channels/). ## Quick Start: Spaces (Collaborative UI) Track participant state for collaborative features: ```typescript import Spaces from '@ably/spaces'; import { SpacesProvider, SpaceProvider, useMembers, useCursors } from '@ably/spaces/react'; // Setup (in root) const spaces = new Spaces(realtimeClient); // Avatar stack function AvatarStack() { const { self, others } = useMembers(); return (
{others.map(member => ( ))}
); } // Live cursors function CursorTracking() { const { set } = useCursors((update) => { // Render other users' cursors renderCursor(update.connectionId, update.position); }); useEffect(() => { const handleMove = (e: MouseEvent) => { set({ position: { x: e.clientX, y: e.clientY } }); }; window.addEventListener('mousemove', handleMove); return () => window.removeEventListener('mousemove', handleMove); }, [set]); return ; } ``` For locations, component locking, and advanced patterns, see [references/spaces/](references/spaces/). ## Quick Start: LiveObjects (Shared State) ⚠️ **Public Preview**: LiveObjects API may change before general availability. Conflict-free shared state synchronization: ```typescript import { LiveCounter, LiveMap } from "ably/liveobjects"; async function setupSharedState() { const channel = realtimeClient.channels.get("game:lobby-1"); const gameState = await channel.object.get(); // Create shared counter await gameState.set("score", LiveCounter.create(0)); // Create shared map await gameState.set( "players", LiveMap.create({ player1: { name: "Alice", ready: false }, player2: { name: "Bob", ready: false }, }), ); // Subscribe to changes gameState.get("score").subscribe(() => { console.log("Score:", gameState.get("score").value()); }); // Update values await gameState.get("score").increment(10); await gameState.get("players").set("player1", { name: "Alice", ready: true }); } ``` React integration: ```typescript function GameLobby() { const [score, setScore] = useState(0); const [players, setPlayers] = useState({}); useEffect(() => { let gameState: any; async function init() { const channel = realtimeClient.channels.get('game:lobby-1'); gameState = await channel.object.get(); // Subscribe to updates gameState.get('score').subscribe(() => { setScore(gameState.get('score').value()); }); gameState.get('players').subscribe(() => { setPlayers(gameState.get('players').value()); }); } init(); return () => { // Cleanup subscriptions }; }, []); return (

Score: {score}

{Object.entries(players).map(([id, player]: [string, any]) => (
{player.name} - {player.ready ? '✓' : '...'}
))}
); } ``` For LiveMap batch operations, composability, and detailed API, see [references/liveobjects/](references/liveobjects/). ## Quick Start: Chat SDK Purpose-built chat with rooms, messages, typing indicators, and reactions: ```typescript import { ChatClient } from '@ably/chat'; import { ChatClientProvider, ChatRoomProvider, useMessages, useTyping } from '@ably/chat/react'; // Setup (in root) const chatClient = new ChatClient(realtimeClient); // Chat component function ChatRoom() { const [messages, setMessages] = useState([]); const { currentlyTyping, keystroke } = useTyping(); const { send, getPreviousMessages } = useMessages({ listener: (event) => { if (event.type === 'created') { setMessages(prev => [...prev, event.message]); } } }); useEffect(() => { // Load history getPreviousMessages({ limit: 50 }).then(result => { setMessages(result.items.reverse()); }); }, []); const handleSend = (text: string) => { send({ text }); }; const handleTyping = () => { keystroke(); // Triggers typing indicator }; return (
{currentlyTyping.length > 0 && ( )}
); } ``` For message updates/deletes, reactions, presence, and room lifecycle, see [references/chat/](references/chat/). ## Quick Start: LiveSync (Database Sync) Broadcast database changes from PostgreSQL/Neon to clients: **Backend (Database + Outbox)**: ```sql -- Outbox table for change events CREATE TABLE outbox ( sequence_id serial PRIMARY KEY, mutation_id TEXT NOT NULL, channel TEXT NOT NULL, name TEXT NOT NULL, data JSONB, processed BOOLEAN DEFAULT false ); -- Trigger to notify on changes CREATE FUNCTION outbox_notify() RETURNS trigger AS $$ BEGIN PERFORM pg_notify('ably_adbc', ''); RETURN NULL; END; $$ LANGUAGE plpgsql; CREATE TRIGGER outbox_trigger AFTER INSERT ON outbox FOR EACH STATEMENT EXECUTE PROCEDURE outbox_notify(); ``` ```typescript // API route - transactional write export async function POST(req: Request) { const { documentId, content } = await req.json(); await db.transaction(async (trx) => { // Update application data await trx("documents") .where({ id: documentId }) .update({ content, updated_at: new Date() }); // Insert change event await trx("outbox").insert({ mutation_id: crypto.randomUUID(), channel: `document:${documentId}`, name: "document.updated", data: { id: documentId, content }, }); }); return Response.json({ success: true }); } ``` **Frontend (Subscribe to Changes)**: ```typescript function DocumentEditor({ documentId }: { documentId: string }) { const [content, setContent] = useState(''); const { channel } = useChannel(`document:${documentId}`, (message) => { if (message.name === 'document.updated') { setContent(message.data.content); } }); useEffect(() => { // Load initial state fetch(`/api/documents/${documentId}`) .then(r => r.json()) .then(doc => setContent(doc.content)); }, [documentId]); return