Realtime
Subscribe to record changes over WebSocket or SSE.
Updated
Railbase pushes record changes to subscribed clients in real time over WebSocket (or Server-Sent Events). Subscriptions respect the same access rules as the REST API — you only receive events for records you could read.
Connect and subscribe
The WebSocket endpoint is GET /api/realtime/ws (subprotocol railbase.v1).
Subscribe by sending a JSON frame with the topics you care about:
{ "action": "subscribe", "topics": ["posts/*", "posts/abc123", "users/me"] }
You can subscribe / unsubscribe at any time without reconnecting.
For SSE, open GET /api/realtime?topics=posts/*,orders/create instead — the
topics ride in the query string, and the connection also speaks the
PocketBase-SDK dialect (a PB_CONNECT hello with a clientId, then
POST /api/realtime to update that client's subscriptions). Both transports
require an authenticated caller.
Topics
Topics are <collection>/<verb> or <collection>/<recordId>:
| Topic | Matches |
|---|---|
posts/* |
every create/update/delete on posts |
posts/create |
only new posts |
posts/abc123 |
changes to one record |
Verbs are create, update, and delete.
Events
The server pushes one frame per matching record event; data carries the verb
and the record:
{
"event": "posts/create",
"id": "<event_id>",
"data": { "action": "create", "record": { "id": "abc123", "title": "…" } }
}
plus control frames (railbase.subscribed, pong, error). Over SSE the same
triple maps onto the event: / id: / data: frame fields.
Note
Each record event is filtered by the collection's View rule, evaluated per
subscriber against that record — the same gate the REST get/list read path
applies — so realtime can't leak a record a user couldn't read over REST. It is
fail-closed: an unknown collection or an unparseable rule denies delivery, and
an empty View rule means "locked" (no one receives it). The check runs on both
the live stream and on resume replay. Record payloads are also projected to the
read-surfaced columns before they go on the wire, so internal columns (password
hashes, token keys) never reach a subscriber. Subscribe-time filter: /
expand: are not supported yet; subscribe to a topic and read details over REST
if you need expansion.
From the SDK
The generated TypeScript client wraps the SSE stream as a typed
async iterator — iterate with for await, and abort to stop:
const ctrl = new AbortController();
for await (const ev of rb.realtime.subscribe<Posts>({
topics: ["posts/*"],
signal: ctrl.signal,
})) {
console.log(ev.id, ev.topic, ev.data);
}
// later: ctrl.abort()
Each ev carries id (the monotonic broker event id — pass the last one seen as
since to resume after a drop), topic (the matched topic), and data (the
decoded payload). Heartbeats and the internal railbase.* control frames are
filtered out, so the loop yields real topic events only.
Publishing your own topics
Server-side (Go), plugins and apps can publish domain events on the bus:
app.Realtime().Publish("orders.shipped", payload, tenantID)
Subscribers receive them the same way as record events.