Skip to main content
@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):
In Next.js + Vercel, omit 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.
With filtering / ordering / limits:
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" } matches 2026-10-01T00:00:00.000Z.
  • A row with no value for the field matches no range.
An operator that cannot apply, such as a number against a string or an unknown operator, matches no rows and logs a warning in development.

db.useQueryOne — single row by ID

A 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:
The server records every 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

The ghost row appears in 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:
Same store + reconciliation as 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, with error and errorCode.
  • retry(id, data?): applies the write again and sends it, with data merged over its fields. It rejects with a MutationRejectedError when 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.
Requires the 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:
The room is named @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 sends POST /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):
When the server moves the player to another shard (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):
These hit the local store optimistically (where applicable) and the server in the background. They use the same path as the hooks.

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):
For long-running clients, run the session auto-refresher to keep tokens fresh:

TypeScript

For end-to-end type safety from schema → hooks, use createTypedDb with your manifest’s generated types:
Run pylon codegen client --target ts to emit Schema from your app.ts.