Skip to content

Realtime — SDK

Aerostack Realtime provides WebSocket-based pub/sub with automatic reconnection, database change notifications, presence tracking, and persistent message history. It is one of the most powerful modules in the SDK and a major differentiator — most backend platforms only offer basic database listeners. Aerostack gives you a full real-time infrastructure layer.

  • Live chat — Pub/sub channels with persistent message history and typing indicators
  • Collaborative editing — Broadcast document changes to all connected editors
  • Presence / “who’s online” — Track connected users with custom state (name, cursor position, status)
  • Live dashboards — DB change events push new metrics to connected dashboard clients
  • Auction / bidding systems — Real-time bid updates with DB-backed consistency
  • Multiplayer game state — Sync player positions, scores, and actions across clients
  • Notification feeds — Push events to specific user channels
  • Live support queues — Track agent availability and route tickets in real-time

Server

Clients

React App

WebSocket

Mobile App

Dashboard

Durable Object
Channel Manager

Pub/Sub
Broadcast

Presence
Tracker

Message
History

Database

Change Tracker

Server SDK
sdk.socket.emit

There are two types of channels:

Channel typeTopic formatHow it works
DB Change Events'orders' (no slash)Automatic — any SQL write to the orders table broadcasts INSERT/UPDATE/DELETE to all subscribers
Custom Pub/Sub'chat/general' (with slash)Manual — clients or servers publish events to the channel

Custom channels let you broadcast events between clients and servers. Use a topic with a slash to distinguish from DB table channels.

import { useAerostack } from '@aerostack/react'
function ChatRoom({ roomId }: { roomId: string }) {
const { realtime } = useAerostack()
const [messages, setMessages] = useState([])
useEffect(() => {
const channel = realtime.channel(`chat/${roomId}`)
channel
.on('message', ({ data }) => {
setMessages(prev => [...prev, data])
})
.on('typing', ({ data }) => {
// Show typing indicator
})
.subscribe()
return () => channel.unsubscribe()
}, [realtime, roomId])
return (
<div>
{messages.map(msg => (
<div key={msg.id}>{msg.user}: {msg.text}</div>
))}
</div>
)
}
const channel = realtime.channel(`chat/${roomId}`)
// Publish a message (broadcast to all subscribers)
channel.publish('message', {
id: crypto.randomUUID(),
user: currentUser.name,
text: 'Hello everyone!',
timestamp: Date.now(),
})
// Publish with persistence (stored in DB for history)
channel.publish('message', { ... }, { persist: true })

Use sdk.socket.emit() to broadcast events from your backend:

// Server-side: broadcast after processing
sdk.socket.emit('message', {
id: crypto.randomUUID(),
user: 'System',
text: 'Order #1234 has been shipped',
}, `notifications/${userId}`)

Subscribe to a database table name (no slash) to receive automatic notifications whenever rows are inserted, updated, or deleted.

React ClientDurable ObjectChange TrackerDatabaseServer SDKReact ClientDurable ObjectChange TrackerDatabaseServer SDKINSERT INTO orders ...Row insertedBroadcast INSERT event{ type: "db_change", operation: "INSERT", data: { ... } }
import { useAerostack } from '@aerostack/react'
function LiveOrders() {
const { realtime } = useAerostack()
const [orders, setOrders] = useState([])
useEffect(() => {
const channel = realtime.channel('orders') // table name, no slash
channel
.on('INSERT', ({ data }) => {
setOrders(prev => [data, ...prev])
})
.on('UPDATE', ({ data, old }) => {
setOrders(prev => prev.map(o => o.id === data.id ? data : o))
})
.on('DELETE', ({ data }) => {
setOrders(prev => prev.filter(o => o.id !== data.id))
})
.subscribe()
return () => channel.unsubscribe()
}, [realtime])
return (
<ul>
{orders.map(order => (
<li key={order.id}>
#{order.id} - {order.status} - ${order.total}
</li>
))}
</ul>
)
}

You do not need to call sdk.socket.emit() for DB changes. The Change Tracker handles it automatically:

// This INSERT automatically broadcasts to all channel('orders') subscribers
await sdk.db.query(
'INSERT INTO orders (id, user_id, total, status) VALUES (?, ?, ?, ?)',
[orderId, userId, 99.99, 'pending']
)
// This UPDATE automatically broadcasts to all channel('orders') subscribers
await sdk.db.query(
'UPDATE orders SET status = ? WHERE id = ?',
['shipped', orderId]
)
interface RealtimePayload<T = any> {
type: 'db_change' | 'event'
topic: string
operation?: 'INSERT' | 'UPDATE' | 'DELETE' // DB changes only
event?: string // Custom events only
data: T // New/current row or event data
old?: T // Previous value (UPDATE and DELETE)
userId?: string
timestamp?: number | string
}

Track who is connected to a channel with custom state. Presence is ideal for “who’s online” indicators, cursor positions in collaborative editors, and agent availability in support systems.

Channel: doc/meeting-notes

presence:join / presence:update

presence:join / presence:update

Alice
{cursor: 42, color: #FF5733}

Bob
{cursor: 87, color: #33FF57}

Carol
{cursor: 15, color: #3357FF}

import { useAerostack, useAuth } from '@aerostack/react'
function OnlineUsers({ roomId }: { roomId: string }) {
const { realtime } = useAerostack()
const { user } = useAuth()
const [online, setOnline] = useState<Map<string, any>>(new Map())
useEffect(() => {
const channel = realtime.channel(`room/${roomId}`)
// Announce your presence
channel.track({
userId: user.id,
name: user.name,
color: '#FF5733',
status: 'active',
})
// Listen for others joining/leaving
channel
.on('presence:join', ({ data }) => {
setOnline(prev => new Map(prev).set(data.userId, data))
})
.on('presence:update', ({ data }) => {
setOnline(prev => new Map(prev).set(data.userId, data))
})
.on('presence:leave', ({ data }) => {
setOnline(prev => {
const next = new Map(prev)
next.delete(data.userId)
return next
})
})
.subscribe()
return () => {
channel.untrack() // Remove your presence
channel.unsubscribe() // Disconnect from channel
}
}, [realtime, roomId, user])
return (
<div>
<h3>{online.size} online</h3>
{[...online.values()].map(u => (
<span key={u.userId} style={{ color: u.color }}>
{u.name}
</span>
))}
</div>
)
}

Call .track() again with new data to update your presence without disconnecting:

// Update cursor position in a collaborative editor
channel.track({
userId: user.id,
name: user.name,
cursor: { line: 42, col: 15 },
selection: { start: 100, end: 150 },
})

When a client disconnects (tab close, network loss), presence is automatically removed and a presence:leave event is broadcast to remaining subscribers.


Persist messages in a channel and retrieve them later. This is essential for chat applications where users need to see messages sent before they connected.

channel.publish('message', {
userId: user.id,
text: 'This message will be saved',
timestamp: Date.now(),
}, { persist: true }) // <-- messages are stored in DB
// Get the last 50 messages
const messages = await channel.getHistory(50)
// Paginate: get 50 messages before a timestamp
const older = await channel.getHistory(50, messages[messages.length - 1].created_at)
interface HistoryMessage {
id: string
room_id: string
user_id: string
event: string
data: any
created_at: number
}

import { useAerostack } from '@aerostack/react'
function ConnectionIndicator() {
const { realtime } = useAerostack()
const [status, setStatus] = useState('idle')
useEffect(() => {
const unsub = realtime.onStatusChange((newStatus) => {
// 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected'
setStatus(newStatus)
})
return unsub
}, [realtime])
const colors = {
connected: 'green',
connecting: 'yellow',
reconnecting: 'orange',
disconnected: 'red',
idle: 'gray',
}
return (
<span style={{ color: colors[status] }}>
{status}
</span>
)
}

The SDK automatically reconnects when the WebSocket connection drops. During reconnection:

  1. Status changes to 'reconnecting'
  2. The SDK uses exponential backoff (1s, 2s, 4s, 8s, max 30s)
  3. On reconnect, all active subscriptions are restored automatically
  4. Status changes back to 'connected'
// The SDK connects automatically on mount, but you can control it manually:
await realtime.connect()
realtime.disconnect()

After a token refresh, update the realtime connection:

realtime.setToken(newAccessToken)

src/components/Chat.tsx
import { useAerostack, useAuth } from '@aerostack/react'
interface ChatMessage {
id: string
userId: string
userName: string
text: string
timestamp: number
}
export function Chat({ roomId }: { roomId: string }) {
const { realtime } = useAerostack()
const { user } = useAuth()
const [messages, setMessages] = useState<ChatMessage[]>([])
const [input, setInput] = useState('')
const [online, setOnline] = useState<Map<string, any>>(new Map())
const channelRef = useRef(null)
useEffect(() => {
const channel = realtime.channel(`chat/${roomId}`)
channelRef.current = channel
// Load message history
channel.getHistory(100).then(history => {
setMessages(history.map(h => h.data))
})
// Track presence
channel.track({
userId: user.id,
name: user.name,
status: 'online',
})
channel
// New messages
.on('message', ({ data }) => {
setMessages(prev => [...prev, data])
})
// Presence events
.on('presence:join', ({ data }) => {
setOnline(prev => new Map(prev).set(data.userId, data))
})
.on('presence:leave', ({ data }) => {
setOnline(prev => {
const next = new Map(prev)
next.delete(data.userId)
return next
})
})
.subscribe()
return () => {
channel.untrack()
channel.unsubscribe()
}
}, [realtime, roomId, user])
const sendMessage = () => {
if (!input.trim() || !channelRef.current) return
channelRef.current.publish('message', {
id: crypto.randomUUID(),
userId: user.id,
userName: user.name,
text: input.trim(),
timestamp: Date.now(),
}, { persist: true })
setInput('')
}
return (
<div>
{/* Online users */}
<div>
{online.size} online: {[...online.values()].map(u => u.name).join(', ')}
</div>
{/* Messages */}
<div style={{ height: 400, overflowY: 'auto' }}>
{messages.map(msg => (
<div key={msg.id}>
<strong>{msg.userName}</strong>: {msg.text}
</div>
))}
</div>
{/* Input */}
<form onSubmit={(e) => { e.preventDefault(); sendMessage() }}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
/>
<button type="submit">Send</button>
</form>
</div>
)
}

src/components/CollaborativeTodos.tsx
import { useAerostack, useAuth } from '@aerostack/react'
export function CollaborativeTodos({ listId }: { listId: string }) {
const { realtime } = useAerostack()
const { user } = useAuth()
const [todos, setTodos] = useState([])
const [editors, setEditors] = useState(new Map())
useEffect(() => {
// DB change channel for the todos table
const dbChannel = realtime.channel('todos')
dbChannel
.on('INSERT', ({ data }) => {
if (data.list_id === listId) {
setTodos(prev => [...prev, data])
}
})
.on('UPDATE', ({ data }) => {
setTodos(prev => prev.map(t => t.id === data.id ? data : t))
})
.on('DELETE', ({ data }) => {
setTodos(prev => prev.filter(t => t.id !== data.id))
})
.subscribe()
// Presence channel for who's editing
const presenceChannel = realtime.channel(`todos/${listId}`)
presenceChannel.track({
userId: user.id,
name: user.name,
viewing: true,
})
presenceChannel
.on('presence:join', ({ data }) => {
setEditors(prev => new Map(prev).set(data.userId, data))
})
.on('presence:leave', ({ data }) => {
setEditors(prev => {
const next = new Map(prev)
next.delete(data.userId)
return next
})
})
.subscribe()
return () => {
dbChannel.unsubscribe()
presenceChannel.untrack()
presenceChannel.unsubscribe()
}
}, [realtime, listId, user])
return (
<div>
<p>Editors: {[...editors.values()].map(e => e.name).join(', ')}</p>
<ul>
{todos.map(todo => (
<li key={todo.id} style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
</li>
))}
</ul>
</div>
)
}

Complete example: live analytics dashboard

Section titled “Complete example: live analytics dashboard”
src/components/LiveMetrics.tsx
import { useAerostack } from '@aerostack/react'
export function LiveMetrics() {
const { realtime } = useAerostack()
const [metrics, setMetrics] = useState({
activeUsers: 0,
ordersToday: 0,
revenue: 0,
})
useEffect(() => {
// Subscribe to the metrics broadcast channel
const channel = realtime.channel('dashboard/metrics')
channel
.on('metrics:update', ({ data }) => {
setMetrics(data)
})
.subscribe()
// Also subscribe to orders table for real-time order count
const ordersChannel = realtime.channel('orders')
ordersChannel
.on('INSERT', ({ data }) => {
setMetrics(prev => ({
...prev,
ordersToday: prev.ordersToday + 1,
revenue: prev.revenue + data.total,
}))
})
.subscribe()
return () => {
channel.unsubscribe()
ordersChannel.unsubscribe()
}
}, [realtime])
return (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 16 }}>
<div>
<h3>Active Users</h3>
<p>{metrics.activeUsers}</p>
</div>
<div>
<h3>Orders Today</h3>
<p>{metrics.ordersToday}</p>
</div>
<div>
<h3>Revenue</h3>
<p>${metrics.revenue.toFixed(2)}</p>
</div>
</div>
)
}

MethodDescription
sdk.socket.emit(event, data, roomId)Broadcast an event to all channel subscribers
realtime.connect()Connect to the WebSocket server
realtime.disconnect()Disconnect from the WebSocket server
realtime.channel(topic, options?)Get or create a channel subscription
realtime.onStatusChange(callback)Listen for connection status changes
realtime.setToken(token)Update the auth token on the connection
MethodDescription
.on(event, callback)Register an event listener (chainable)
.off(event, callback)Remove a specific event listener
.subscribe()Start receiving events from this channel
.unsubscribe()Stop receiving events and remove all listeners
.publish(event, data, options?)Broadcast a custom event
.track(state)Announce presence with custom state
.untrack()Remove your presence
.getHistory(limit?, before?)Fetch persisted messages
EventSourcePayload
'INSERT'DB change{ data: newRow }
'UPDATE'DB change{ data: newRow, old: previousRow }
'DELETE'DB change{ data: deletedRow }
'*'AnyAll events on the channel
'presence:join'Presence{ data: userState }
'presence:update'Presence{ data: userState }
'presence:leave'Presence{ data: userState }
Custom stringPub/Sub{ data: publishedPayload }