Pick the right primitive
| You want | Reach for | Persisted? | Deep dive |
|---|---|---|---|
| A live list/row of one entity, filtered | db.useQuery | ✅ synced replica | Live queries |
| A server-side join / aggregate / computed value, live | db.useReactiveQuery | ✅ (re-derived) | Reactive queries |
| Live full-text + faceted search | db.useSearch | ✅ | Search |
| An instant write that reconciles with the server | db.useMutation | ✅ | Optimistic updates |
| Presence — cursors, typing, “who’s here”, broadcast | useRoom | ❌ ephemeral | React client |
| Authoritative multiplayer sim (game / MMO tick loop) | useShard | ❌ in-memory sim | React client |
| A connection / sync status indicator | useSyncStatus | — | React client |
Two layers: synced data vs. 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 last rows —useRoom (presence/broadcast) and useShard (simulation) — are
ephemeral signals. A cursor position or a game tick has no business in your
database; it’s broadcast to the room’s current members and forgotten. Don’t model
presence as an entity, and don’t try to drive a 60fps game loop through db.useQuery.
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, “12 people viewing”. The reliable way to
build it is a public, PII-free projection entity subscribed with db.useQuery,
not 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 where it shines — a server-side join or rollup rendered in a
single (leader) view, like a feed that joins Post → User. Just don’t lean on it for
cross-tab fan-out of a shared value.
How it works
Under the hood every subscription rides one WebSocket per client. The server keeps an index of active subscriptions keyed by entity + indexed filter fields, so a write fans out in O(matching subs), not O(all subs). 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 workhorse and how fan-out scales.React client
Every realtime hook the client exposes, including presence and shards.