Pick the right primitive
Two layers: synced data and ephemeral signals
The first four rows above are synced data. They flow through the entity store, respect policies, and land in the local replica, so they survive reload and work offline. The remaining rows are ephemeral signals:useRoom (presence and
broadcast), the two server-push primitives, and useShard (simulation).
A cursor position or a game tick does not belong in your database. The
server broadcasts it to the room’s current members and then forgets it.
Do not model presence as an entity, and do not drive a 60fps game loop
through db.useQuery.
Streaming server output: one client, or all of them
Both server-push rows above run inside a mutation or action (neither exists on query ctx). They differ in who receives the output:ctx.stream.write(text)writes to the HTTP response of the call in flight. It reaches exactly one client: whoever made the request, usingdb.streamFn(fn, args)to read it back. If you close the tab, the output is gone.ctx.rooms.broadcast(room, topic, data)pushes over the WebSocket to every subscriber of a presence room, whether or not they made the call. It resolves{ delivered: false }when the room has no members. An empty room causes no error.
ctx.stream.write for a chat box waiting on its own answer. Use
ctx.rooms.broadcast when the output has to reach more than the
caller:
- an agent whose tokens must survive a reload
- a second device following the same session
- a scheduled job that has no HTTP caller at all
useRoom(roomId, userId) and read pushed messages off
the sync engine:
Best practice: cross-tab live state goes through db.useQuery
The most common realtime feature is a shared scalar that must update
everywhere at once: a live counter, remaining capacity, or “12 people
viewing.” Build it with a public, PII-free projection entity subscribed
with db.useQuery, not with a reactive query.
Reactive server queries (db.useReactiveQuery) behave as leader-tab-only
in practice: a follower tab’s reactive subscription may never deliver
its initial result. Entity sync (db.useQuery) reaches every tab. So:
- Keep the sensitive table deny-all:
allowRead: "false". - Have the mutation also maintain a tiny projection entity with
allowRead: "true"and client writes denied. - Subscribe the UI to the projection with
db.useQuery.
db.useReactiveQuery for its strength: a server-side join or rollup
rendered in a single leader view, like a feed that joins Post to
User. Do not rely on it for cross-tab fan-out of a shared value.
How it works
Internally, each subscription rides one WebSocket per client. The server keeps an index of active subscriptions keyed by entity and indexed filter fields, so a write fans out in O(matching subscriptions), not O(all subscriptions). On multi-machine deployments, a cluster bus forwards change events between nodes. A mutation on machine A updates subscribers on machine B. See horizontal scaling for the architecture.Next
Live queries
The
db.useQuery hook and how fan-out scales.React client
Every realtime hook the client exposes, including presence and shards.