Realtime WebSocket

Subscribe to record changes and presence over a WebSocket — connection, message protocol, the invalidation-only model, and limits.

Connect#

wss://api.example.com/ws/realtime?token=<JWT>

The JWT (not a PAT) is passed as a query parameter on the upgrade; the socket is closed with code 4001 when the token expires — reconnect with a fresh one. The tenant comes from the token.

Client messages#

// one message per frame
{ "action": "subscribe",   "collection": "invoices" }
{ "action": "unsubscribe", "collection": "invoices" }
{ "action": "presence.join",  "resource": "record:invoices/9d2c…" }
{ "action": "presence.leave", "resource": "record:invoices/9d2c…" }
{ "action": "chat.join",  "conversationId": "…" }
{ "action": "chat.leave", "conversationId": "…" }

Server messages#

// one message per frame
{ "action": "subscribed", "collection": "invoices" }
{ "action": "error", "message": "Subscription limit reached" }

{ "event": "record.changed", "collection": "invoices", "changeType": "UPDATED",
  "recordId": "9d2c…", "data": { /* the record */ }, "timestamp": "2026-09-21T10:00:00Z" }

{ "event": "presence.changed", "resource": "record:invoices/9d2c…", "users": [ { "id": "…", "email": "…" } ], "timestamp": "…" }

Invalidation, not replication#

Treat record.changed as a signal to refetch through the API, which applies your permissions and field-level security. data is included as a convenience but is omitted whenever the record has masked fields, and it is not filtered per subscriber. The app’s data layer works this way: an event invalidates the cached query, which refetches.

Events are fanned out from the platform’s NATS stream to every gateway pod, so a subscriber receives each change once regardless of which pod it is connected to.

Presence#

Join a record resource to appear in its viewer list and receive presence.changed as others join, leave or time out (30-second heartbeat, 90-second expiry). At most 10 presence resources per socket; snapshots list up to 20 viewers.

Limits#

50 subscriptions per socket, 100 concurrent connections per tenant, 20 chat conversations per socket. Exceeding one answers an error message and ignores the request.

Outbound alternatives#

For server-to-server delivery use outbound webhooks or a NATS_TRIGGERED flow.

Edit this page on GitHub