SDK (Beta)Realtime

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

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 DB

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

  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'

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

src/components/Chat.tsx
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

src/components/CollaborativeTodos.tsx
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

src/components/LiveMetrics.tsx
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

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

Channel methods

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

Events

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 }