---
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