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.
What you can build
Section titled “What you can build”- 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
Architecture
Section titled “Architecture”There are two types of channels:
| Channel type | Topic format | How 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 |
Pub/Sub channels
Section titled “Pub/Sub channels”Custom channels let you broadcast events between clients and servers. Use a topic with a slash to distinguish from DB table channels.
Subscribe to a channel (React)
Section titled “Subscribe to a channel (React)”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> )}Publish to a channel (client-side)
Section titled “Publish to a channel (client-side)”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 })Publish from the server
Section titled “Publish from the server”Use sdk.socket.emit() to broadcast events from your backend:
// Server-side: broadcast after processingsdk.socket.emit('message', { id: crypto.randomUUID(), user: 'System', text: 'Order #1234 has been shipped',}, `notifications/${userId}`)DB change events
Section titled “DB change events”Subscribe to a database table name (no slash) to receive automatic notifications whenever rows are inserted, updated, or deleted.
React: subscribe to table changes
Section titled “React: subscribe to table changes”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> )}Server: just run the SQL
Section titled “Server: just run the SQL”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') subscribersawait 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') subscribersawait sdk.db.query( 'UPDATE orders SET status = ? WHERE id = ?', ['shipped', orderId])Event payload
Section titled “Event payload”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}Presence
Section titled “Presence”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.
Track presence
Section titled “Track presence”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> )}Update presence state
Section titled “Update presence state”Call .track() again with new data to update your presence without disconnecting:
// Update cursor position in a collaborative editorchannel.track({ userId: user.id, name: user.name, cursor: { line: 42, col: 15 }, selection: { start: 100, end: 150 },})Auto-cleanup
Section titled “Auto-cleanup”When a client disconnects (tab close, network loss), presence is automatically removed and a presence:leave event is broadcast to remaining subscribers.
Message history
Section titled “Message history”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.
Publish with persistence
Section titled “Publish with persistence”channel.publish('message', { userId: user.id, text: 'This message will be saved', timestamp: Date.now(),}, { persist: true }) // <-- messages are stored in DBRetrieve history
Section titled “Retrieve history”// Get the last 50 messagesconst messages = await channel.getHistory(50)
// Paginate: get 50 messages before a timestampconst older = await channel.getHistory(50, messages[messages.length - 1].created_at)HistoryMessage type
Section titled “HistoryMessage type”interface HistoryMessage { id: string room_id: string user_id: string event: string data: any created_at: number}Connection management
Section titled “Connection management”Connection status
Section titled “Connection status”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> )}Auto-reconnect
Section titled “Auto-reconnect”The SDK automatically reconnects when the WebSocket connection drops. During reconnection:
- Status changes to
'reconnecting' - The SDK uses exponential backoff (1s, 2s, 4s, 8s, max 30s)
- On reconnect, all active subscriptions are restored automatically
- Status changes back to
'connected'
Manual connect/disconnect
Section titled “Manual connect/disconnect”// The SDK connects automatically on mount, but you can control it manually:await realtime.connect()realtime.disconnect()Update auth token
Section titled “Update auth token”After a token refresh, update the realtime connection:
realtime.setToken(newAccessToken)Complete example: live chat application
Section titled “Complete example: live chat application”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> )}Complete example: collaborative todo list
Section titled “Complete example: collaborative todo list”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”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> )}Server SDK: realtime methods
Section titled “Server SDK: realtime methods”| Method | Description |
|---|---|
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 |
Channel methods
Section titled “Channel methods”| Method | Description |
|---|---|
.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 |
Events
Section titled “Events”| Event | Source | Payload |
|---|---|---|
'INSERT' | DB change | { data: newRow } |
'UPDATE' | DB change | { data: newRow, old: previousRow } |
'DELETE' | DB change | { data: deletedRow } |
'*' | Any | All events on the channel |
'presence:join' | Presence | { data: userState } |
'presence:update' | Presence | { data: userState } |
'presence:leave' | Presence | { data: userState } |
| Custom string | Pub/Sub | { data: publishedPayload } |