Railbase
GPTClaude

Realtime

Subscribe to record changes over WebSocket or SSE.

Updated

Video guide —watch on YouTube ↗

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.

Was this page helpful?Thanks for your feedback!