Live Queries
Connect to ws://<host>/live and subscribe to a query by name. The server pushes
fresh results whenever a mutation writes a table that the query reads — whether the
write came over HTTP or over the socket. Because a Durable Object sees every write
to its store (single-writer), invalidation is exact: only subscriptions whose
read set intersects the mutation's write set are re-run.
Subscriptions inherit the connecting identity, so every push respects the same row-level scope and field projection as a normal read.
Protocol
// client -> server
{ "type": "subscribe", "id": "s1", "name": "listNotes" }
{ "type": "call", "id": "c1", "name": "createNote", "input": { "title": "hi", "body": "x" } }
{ "type": "unsubscribe", "id": "s1" }
// server -> client
{ "type": "data", "id": "s1", "result": [ /* ... */ ] } // initial + every update
{ "type": "result", "id": "c1", "result": { /* ... */ } } // reply to a call
{ "type": "error", "id": "s1", "error": "..." }
The WebSocket uses Cloudflare's hibernation API, so idle connections don't pin the DO in memory.
Browser tokens
Browser WebSockets can't set headers, so /live also accepts the bearer token and
tenant via the query string (?token=...&tenant=...); the Worker folds them into
headers for the rest of the flow. The @pramen/client library handles this for you —
see Clients.
Token expiry
The token is verified once, at upgrade — the socket's identity is fixed from then
on. So that a long-lived (or hibernating) connection can't outlive its session, the DO
re-checks the token's exp on every message: an expired socket gets an unauthorized
error frame, is closed with code 4401, and stops receiving pushes.
Treat 4401 as "re-authenticate, then reconnect". Calling setToken on
@pramen/client with a token from
refreshSession resets the socket for you.
Revoking a user (deactivate/delete) blocks the upgrade via the denylist, so a
revoked client is cut off as soon as it reconnects.