Realtime
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.
Beta — Realtime APIs are stable but may receive non-breaking additions.
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
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
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)
import { useAerostack } from '@aerostack/react'
import { useEffect, useState } from '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)
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
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}`)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
import { useAerostack } from '@aerostack/react'
import { useEffect, useState } from '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
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]
)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
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
import { useAerostack, useAuth } from '@aerostack/react'
import { useEffect, useState } from '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
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 },
})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
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
channel.publish('message', {
userId: user.id,
text: 'This message will be saved',
timestamp: Date.now(),
}, { persist: true }) // <-- messages are stored in DBRetrieve history
// 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)HistoryMessage type
interface HistoryMessage {
id: string
room_id: string
user_id: string
event: string
data: any
created_at: number
}Connection management
Connection status
import { useAerostack } from '@aerostack/react'
import { useEffect, useState } from '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
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
// The SDK connects automatically on mount, but you can control it manually:
await realtime.connect()
realtime.disconnect()Update auth token
After a token refresh, update the realtime connection:
realtime.setToken(newAccessToken)Complete example: live chat application
import { useAerostack, useAuth } from '@aerostack/react'
import { useEffect, useState, useRef } from '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
import { useAerostack, useAuth } from '@aerostack/react'
import { useEffect, useState } from '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
import { useAerostack } from '@aerostack/react'
import { useEffect, useState } from '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
| 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
| 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
| 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 } |