RafikiDB Realtime#

realtime

Use when working with RafikiDB realtime: SSE events, Centrifugo WebSocket connections, realtime/connect tokens, channels, event payloads, subscribe patterns across all SDKs, or debugging why realtime events are not delivered.

You are working with RafikiDB realtime. Two transports deliver live database changes to clients: Centrifugo WebSocket (recommended for scale) and SSE.

How it works#

  1. Client requests a scoped token: POST /api/v1/projects/{projectID}/realtime/connect?table=<table> (API key or project-user auth). Response: { url, token, channel, expires_at }.
    • channel = project:{projectID}:{table}
    • token = JWT valid 24h; subs claim pre-scopes the channel
    • subject = project:{id} for API keys, user:{id} for project users
  2. Client opens a WebSocket to {url}/connection/websocket and sends the Centrifugo v6 wire command: {"id": 1, "connect": {"token": "<token>"}}. The token's subs claim auto-subscribes the channel, so no explicit subscribe command is needed.
  3. Server pushes frames { "push": { "channel": "...", "pub": { "data": {...} } } }. Match push.channel against your channel, then read push.pub.data.
  4. On close/error, reconnect with exponential backoff (1s, 2s, 4s ... max 30s) and resubscribe.

Event payload#

{
  "id": "375baab2-...",
  "type": "INSERT",
  "table": "messages",
  "project_id": "5c73dab0-...",
  "record": { "id": "...", "text": "hi" },
  "old_record": { "id": "...", "text": "old" },
  "created_at": "2026-10-03T21:20:00Z"
}
JSON
  • type: INSERT | UPDATE | DELETE
  • old_record present only on UPDATE (DELETE events carry the pre-delete row in record)
  • Deletes are soft deletes (deleted_at), so rows still exist after a DELETE event.

SSE transport#

GET /api/v1/projects/{projectID}/realtime/events?api_key=<key>&table=<table>&events=INSERT,UPDATE,DELETE

  • Auth via query params because EventSource cannot send headers: ?token=<realtime-token> first, then ?api_key=<raw key>.
  • Frames: id: <uuid> / event: INSERT|UPDATE|DELETE / data: <json>.
  • Heartbeat comment every 25s (browsers/EventSource handle this automatically).
  • Fallback only: no backpressure guarantees, single table.

Settings and channels (dashboard, JWT)#

  • GET/PUT /realtime/settings -> {enabled, broadcast_inserts, broadcast_updates, broadcast_deletes}
  • GET/POST /realtime/channels, DELETE /realtime/channels/:channelID (channel: {name, table_id?, enabled})
  • POST /realtime/token -> 10-minute TTL realtime token (JWT, Redis-backed)

SDK patterns#

JS/TS:

const sub = db.realtime.subscribe("messages", (e) => console.log(e.type, e.record), {
  events: ["INSERT", "DELETE"],
  transport: "auto", // auto | centrifugo | sse
});
sub.unsubscribe();
TypeScript

Dart:

final sub = db.realtime.subscribe("messages", (e) => print(e.record),
  options: const SubscribeOptions(events: ["INSERT"], transport: RealtimeTransport.auto));
sub.unsubscribe();
DART

PHP:

$sub = $db->realtime->subscribe('messages', function (array $event) {}, ['events' => ['INSERT']]);
$sub->run();  // blocking, worker process
PHP

Python:

sub = db.realtime.subscribe("messages", on_event, events=["INSERT"], transport="auto")
sub.close()
Python

Gotchas#

  • Realtime only delivers events for tables with realtime enabled in settings, and broadcasts respect broadcast_inserts/updates/deletes flags.
  • The Centrifugo JWT comes from your API (never hardcode a Centrifugo token in clients).
  • transport: "auto" prefers Centrifugo WebSocket when the runtime supports it, else SSE.
  • WebSocket URL derives from url by replacing http -> ws and appending /connection/websocket.