@pylonsync/react is the React client. The db.* namespace exposes hooks that read from a local sync replica. Every component using a hook re-renders automatically when a row changes (your own mutation, a server push, a write from another tab).
Install
Initialize once
Pick any entry point that runs before your first hook (src/main.tsx, a Next.js root client provider, a top-level component effect):
baseUrl (or pass "") so the client talks same-origin and the Next rewrite forwards /api/* to your Pylon backend.
The db.* namespace
db is the public API for apps using a single global sync engine. Every method below subscribes to or writes to the same shared replica.
db.useQuery — live list
loading is true only while there is nothing to show and the first server pull has not landed. With a warm local cache it is false at once, even before this session has talked to the server. synced is true once the engine has completed a pull in this session (it goes back to false after an org or account switch until the re-pull lands, and stays false while offline). Check it where a partial list would mislead: counts, “no results”, billing state.
where takes a value (equality) or operators: $gt, $gte, $lt, $lte, $not, $in, $like. The query runs on the local replica and gives the same rows as the server:
- Numbers compare as numbers.
- Strings compare by character code, so
"B"comes before"a". - Two RFC 3339 timestamps compare as times, whatever their offset or precision.
occurredAt: { $gte: "2026-10-01T00:00:00Z" }matches2026-10-01T00:00:00.000Z. - A row with no value for the field matches no range.
db.useQueryOne — single row by ID
null row with synced: false may still exist on the server.
db.useInfiniteQuery — paginated with loadMore
db.useReactiveQuery — server-side query, auto re-runs
For Convex-style server functions that the framework re-runs whenever their dependency set changes:
ctx.db.* read inside your query() handler and re-runs the handler when any of those rows mutate. See Reactive queries.
db.useMutation — server function with optimistic updates
db.useQuery("Message", ...) instantly; the server’s broadcast reconciles in-place. See Optimistic updates.
db.useEntity — optimistic CRUD bound to one entity
When you’d rather call insert/update/delete directly than wire a server function:
db.useMutation. The change is in the local store before the promise settles. The promise resolves when the server applies the write, or when the server can’t be reached and the write is queued for retry. It rejects with a MutationRejectedError when the server refuses the write (policy denial, validation); the optimistic change is rolled back first.
useOutbox — unsynced and failed writes
useOutbox() lists the writes on this device that are not on the server. It re-renders when a write is queued, applied, rejected, retried, or dismissed.
pending: writes made offline, or waiting for a sign-in. Oldest first.failed: writes the server rejected, witherroranderrorCode.retry(id, data?): applies the write again and sends it, withdatamerged over its fields. It rejects with aMutationRejectedErrorwhen the server rejects it again, or when another user is signed in.dismiss(id): drops a failed write.
queueUpload and useFileUrl — photos taken offline
queueUpload(file, { attach }) keeps a file on the device and uploads it when there is a network, then sets it on its record. useFileUrl(value) shows a local: value from the copy on the device. See Offline apps.
useAppUpdate — a new deploy is available
In an app with buildManifest({ offline }), useAppUpdate() reports when a newer build is deployed. apply() waits until the outbox has no pending writes, then loads the new build. <AppUpdateBar /> is a minimal notice that uses it. See Offline apps.
db.useSearch — live faceted search
search plugin enabled per-entity in your schema. See Search.
db.useAggregate — live count / sum / avg / groupBy
count is "*" (or a column name for COUNT(col)); sum / avg / min / max take arrays of column names; groupBy takes an array of columns (or date-bucket specs). data is one row per group.
Sessions
useSession returns a live auth object that re-renders on sign-in, sign-out, org switch, or remote session revoke. It also exposes selectOrg, clearOrg, and refresh for multi-tenant flows.
Connection status
Presence + rooms
Rooms tied to a row
A free room name (chat-42, doc:${docId}) is open to any signed-in user who knows it. For a row’s presence, pass the row instead:
@Document/<id> (rowRoom(entity, id) builds the name), and the server runs the row’s read policy on every room operation: join, presence, heartbeat, messages, the member list, the room list and the WebSocket subscription. A missing row and a row the caller may not read get the same answer, ROOM_FORBIDDEN. A name that starts with @ and does not name a row is refused (INVALID_ROOM), so @ cannot start a free room.
Someone who loses access while in the room (the document is unshared) stops receiving its events within 5 seconds of the change. Within 10 seconds the server removes them from the room, even when nobody in it acts: the others get their leave, and their own hook sets denied, clears peers, and stops rejoining. A member on the HTTP polling fallback (no WebSocket) is removed at their next heartbeat.
On a deployment with more than one machine, room membership is kept per machine. When someone connected through two machines closes the connection on one, people on the other machines can stop seeing them until their next presence update.
Joining and staying in a room
The hook joins over HTTP first and subscribes over the WebSocket after the join lands. While joined it sendsPOST /api/rooms/heartbeat every 45 s; the server drops a member after 120 s without activity and tells the room. When the server reports the member gone (a heartbeat returns alive: false, or a resubscribe after a reconnect gets NOT_IN_ROOM), the hook rejoins and resubscribes. A closed socket removes the user only from rooms no other socket of that user still holds.
Multiplayer shards
For tick-driven multiplayer (games, collaborative canvases):ctx.shards.transfer), the hook reconnects there on its own; its
shardId field is the shard it is connected to. When a deploy moves the
shard itself to another machine, the hook reconnects the same way and its
shardId stays. See
Move a player to another shard
and Deploys.
See Live queries → shards.
Imperative calls
For one-shot reads/writes outside the React tree (effects, event handlers, server-rendered code):Auth helpers
For sign-in flows that the framework owns end-to-end (cookie + storage update + session refresh), use the@pylonsync/next/auth helpers (works outside Next.js too; the auth module has no Next-specific deps despite the package path):
TypeScript
For end-to-end type safety from schema → hooks, usecreateTypedDb with your manifest’s generated types:
pylon codegen client --target ts to emit Schema from your app.ts.