# Realtime — SDK

> WebSocket pub/sub, database change events, presence tracking, and message history. Build live chat, collaborative editing, dashboards, and multiplayer features.

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

```mermaid
flowchart LR
    subgraph Clients
        A[React App] --> WS[WebSocket]
        B[Mobile App] --> WS
        C[Dashboard] --> WS
    end

    WS --> DO[Durable Object\nChannel Manager]

    subgraph Server
        DO --> PUB[Pub/Sub\nBroadcast]
        DO --> PRES[Presence\nTracker]
        DO --> HIST[Message\nHistory]
        DB[(Database)] --> CT[Change Tracker]
        CT --> DO
    end

    SDK[Server SDK\nsdk.socket.emit] --> DO
```

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)

```tsx

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)

```ts
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:

```ts
// 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.

```mermaid
sequenceDiagram
    participant Server as Server SDK
    participant DB as Database
    participant CT as Change Tracker
    participant DO as Durable Object
    participant Client as React Client

    Server->>DB: INSERT INTO orders ...
    DB-->>CT: Row inserted
    CT->>DO: Broadcast INSERT event
    DO->>Client: { type: "db_change", operation: "INSERT", data: { ... } }
```

### React: subscribe to table changes

```tsx

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:

```ts
// 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

```ts
interface RealtimePayload {
  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.

```mermaid
flowchart LR
    subgraph Channel: doc/meeting-notes
        U1["Alice\n{cursor: 42, color: #FF5733}"]
        U2["Bob\n{cursor: 87, color: #33FF57}"]
        U3["Carol\n{cursor: 15, color: #3357FF}"]
    end

    U1 -- "presence:join / presence:update" --> U2
    U1 -- "presence:join / presence:update" --> U3
```

### Track presence

```tsx

function OnlineUsers({ roomId }: { roomId: string }) {
  const { realtime } = useAerostack()
  const { user } = useAuth()
  const [online, setOnline] = useState>(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:

```ts
// 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

```ts
channel.publish('message', {
  userId: user.id,
  text: 'This message will be saved',
  timestamp: Date.now(),
}, { persist: true })  // <-- messages are stored in DB
```

### Retrieve history

```ts
// 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

```ts
interface HistoryMessage {
  id: string
  room_id: string
  user_id: string
  event: string
  data: any
  created_at: number
}
```

---

## Connection management

### Connection status

```tsx

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

```ts
// 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:

```ts
realtime.setToken(newAccessToken)
```

---

## Complete example: live chat application

```tsx title="src/components/Chat.tsx"

interface ChatMessage {
  id: string
  userId: string
  userName: string
  text: string
  timestamp: number
}

  const { realtime } = useAerostack()
  const { user } = useAuth()
  const [messages, setMessages] = useState([])
  const [input, setInput] = useState('')
  const [online, setOnline] = useState>(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>
      
      <div>
        {online.size} online: {[...online.values()].map(u => u.name).join(', ')}
      </div>

      
      <div style={{ height: 400, overflowY: 'auto' }}>
        {messages.map(msg => (
          <div key={msg.id}>
            <strong>{msg.userName}</strong>: {msg.text}
          </div>
        ))}
      </div>

      
      <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

```tsx title="src/components/CollaborativeTodos.tsx"

  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

```tsx title="src/components/LiveMetrics.tsx"

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