Realtime
See Concepts → Realtime for the pub/sub model behind these two endpoints.
GET /realtime — opens an SSE stream
Section titled “GET /realtime — opens an SSE stream”Content-Type: text/event-stream. The first event, PB_CONNECT,
carries { "clientId": "..." }. Every subsequent message event carries
{ "action": "create" | "update" | "delete", "record": { ... } } for
whatever topics the client has subscribed to. The connection stays open
until the client disconnects.
POST /realtime — declare subscriptions
Section titled “POST /realtime — declare subscriptions”{ "clientId": "abc123...", "subscriptions": ["posts", "posts/RECORD_ID"] }A bare collection name subscribes to every change in it (filtered by
that collection’s listRule per-event); collection/recordId
subscribes to one record only. 204 on success; 404 if clientId
names a connection that’s already closed. Calling this again with a new
subscriptions array replaces the client’s subscription set — it
does not merge with a previous call.
Two realtime event kinds outside this pub/sub model ride the same GET /realtime connection: llm_chunk/llm_done/llm_error from the
LLM chat gateway, keyed by clientId
rather than a collection subscription.
Channels
Section titled “Channels”See Concepts → Realtime channels & presence
for the full authorization model. A channel is a topic not tied to any
record: subscribe to it the same way as a collection, with the literal
string channel:<name> in POST /realtime’s subscriptions array —
e.g. { "clientId": "...", "subscriptions": ["channel:room1"] }.
POST /realtime/channels/{name}/publish
Section titled “POST /realtime/channels/{name}/publish”{ "event": "chat", "data": { "text": "hi" } }Broadcasts {event, data} to every current subscriber of channel:{name},
on every node sharing this database. 204 on success. Requires the
channel’s _channels row to have a publishRule that allows this
caller — with no matching row at all, the channel is disabled and this
is 403. data (plus event) is capped at ~7.5KB; a larger publish is
refused with 413 rather than silently failing cross-node delivery
(Postgres’s own NOTIFY payload cap is 8000 bytes).
POST /realtime/channels/{name}/presence
Section titled “POST /realtime/channels/{name}/presence”{ "clientId": "abc123...", "state": { "name": "Ada", "cursor": [120, 84] } }A presence heartbeat: clientId must be a connection already open via
GET /realtime. Every subscriber of channel:{name} receives a
presence.join (first heartbeat) or presence.update (later ones) event
with { clientId, state, auth }. Call this again on an interval (a few
times more often than 45s) to stay listed — a member is automatically
removed, and every subscriber gets a presence.leave, when its GET /realtime connection disconnects or 45 seconds pass with no heartbeat.
200 with { "event": "presence.join" | "presence.update" } on
success; 404 if clientId isn’t a live connection; 403 if this
channel’s subscribeRule doesn’t allow this caller.
GET /realtime/channels/{name}/presence
Section titled “GET /realtime/channels/{name}/presence”{ "members": [{ "clientId": "abc123...", "state": { "name": "Ada" }, "auth": { "id": "u1", "collectionName": "users" } }] }The channel’s current members, gated by the same subscribeRule as
tracking presence.
GET /realtime/channels/{name}/stats
Section titled “GET /realtime/channels/{name}/stats”Superuser/API-key only: { "subscribers": 3, "presence": 1 } — this
node’s own live counts, for the dashboard’s Realtime channels inspector.
Node-local, not a cluster-wide total.