Skip to main content

Documentation

WebSocket API Protocol

Complete WebSocket message specification

WebSocket API Protocol

🔌 Complete WebSocket Specification

Connection

WS /v1/realtime

Implemented in src/realtime/mod.rs. On connect, table-level changes are delivered via Postgres LISTEN/NOTIFY fanned out to subscribers through in-process tokio::broadcast channels — not logical replication/WAL streaming.

Message Format

All messages are JSON. Client and server messages have different shapes (there is no shared envelope with a key/value/seq structure).

Client → Server Message Types

1. Subscribe

{
  "type": "subscribe",
  "table": "users",
  "event": "*",
  "filter": "..."
}

table is required. event (e.g. "INSERT", "UPDATE", "DELETE", "*") and filter are accepted in the message but are not currently enforced by the server — a subscription receives every INSERT/UPDATE/DELETE event for the table regardless of what you pass for event/filter. Filter client-side until this is implemented server-side.

2. Unsubscribe

{
  "type": "unsubscribe",
  "table": "users"
}

3. Ping

{"type": "ping"}

Server → Client Message Types

1. Connected (sent immediately on connect)

{
  "type": "connected",
  "message": "Connected to Stackhouse Realtime",
  "client_id": 1
}

2. Subscribed / Unsubscribed (ack)

{"type": "subscribed", "table": "users", "event": "*"}
{"type": "unsubscribed", "table": "users"}

3. Data events

{
  "type": "INSERT",
  "table": "users",
  "record": {"id": 1, "name": "Alice"},
  "timestamp": "2025-01-03T12:00:00Z"
}
{
  "type": "UPDATE",
  "table": "users",
  "record": {"id": 1, "name": "Alice B."},
  "old_record": {"id": 1, "name": "Alice"},
  "timestamp": "2025-01-03T12:00:00Z"
}
{
  "type": "DELETE",
  "table": "users",
  "old_record": {"id": 1, "name": "Alice B."},
  "timestamp": "2025-01-03T12:00:00Z"
}

record is present for INSERT/UPDATE, old_record for UPDATE/DELETE.

4. Error

{
  "type": "error",
  "message": "Invalid message format: ..."
}

Sent for malformed JSON or an unrecognized type.

5. Pong

{"type": "pong"}

Best Practices

  1. Handle reconnection
ws.addEventListener('close', () => {
  setTimeout(() => {
    ws = new WebSocket(url);
  }, 1000);
});
  1. Resubscribe on reconnect
const tables = ['users', 'documents'];
ws.onopen = () => {
  tables.forEach(table => {
    ws.send(JSON.stringify({ type: 'subscribe', table, event: '*' }));
  });
};
  1. Error handling
ws.addEventListener('error', (error) => {
  console.error('WebSocket error:', error);
});

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === 'error') {
    console.error('Realtime error:', msg.message);
  }
};

Presence tracking and channel broadcast are separate REST endpoints (not WebSocket messages), also mounted under /v1/realtime — see src/realtime/presence.rs and src/realtime/broadcast.rs:

MethodPath
POST/v1/realtime/presence/track
POST/v1/realtime/presence/untrack
GET/v1/realtime/presence/:channel
GET/v1/realtime/presence
POST/v1/realtime/broadcast/send
GET/v1/realtime/broadcast/channels
GET/v1/realtime/broadcast/:channel/history

Done! 🎉