RafikiDB Realtime#
You are working with RafikiDB realtime. Two transports deliver live database changes to clients: Centrifugo WebSocket (recommended for scale) and SSE.
How it works#
- 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;subsclaim pre-scopes the channel- subject =
project:{id}for API keys,user:{id}for project users
- Client opens a WebSocket to
{url}/connection/websocketand sends the Centrifugo v6 wire command:{"id": 1, "connect": {"token": "<token>"}}. The token'ssubsclaim auto-subscribes the channel, so no explicitsubscribecommand is needed. - Server pushes frames
{ "push": { "channel": "...", "pub": { "data": {...} } } }. Matchpush.channelagainst your channel, then readpush.pub.data. - 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"
}
JSONtype: INSERT | UPDATE | DELETEold_recordpresent only on UPDATE (DELETE events carry the pre-delete row inrecord)- 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();
TypeScriptDart:
final sub = db.realtime.subscribe("messages", (e) => print(e.record),
options: const SubscribeOptions(events: ["INSERT"], transport: RealtimeTransport.auto));
sub.unsubscribe();
DARTPHP:
$sub = $db->realtime->subscribe('messages', function (array $event) {}, ['events' => ['INSERT']]);
$sub->run(); // blocking, worker process
PHPPython:
sub = db.realtime.subscribe("messages", on_event, events=["INSERT"], transport="auto")
sub.close()
PythonGotchas#
- Realtime only delivers events for tables with realtime enabled in settings, and broadcasts respect
broadcast_inserts/updates/deletesflags. - 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
urlby replacinghttp->wsand appending/connection/websocket.