@pylonsync/sync is the engine the React, React Native, and Next.js clients all wrap. You can use it standalone in any JavaScript host — Vue, Svelte, Solid, vanilla JS, Node, Bun, Tauri, Electron, Cloudflare Workers.
This page covers the engine’s mental model, configuration, and direct API. For React-specific hooks see React.
Mental model
The sync engine maintains an in-memory replica of the rows the server has shown you. That replica is:- Server-authoritative — every change has a
seqnumber from the server’s append-only change log - Tombstone-aware — deletes can’t be resurrected by an out-of-order replay
- Optimistic — local mutations update the replica immediately; a background queue ships them to the server with idempotency tokens
- Identity-flip-safe — when the auth token or active tenant changes, the replica resets so you don’t see stale rows from the previous identity
- Crash-safe — the persistence layer (IndexedDB on web, SQLite on native) writes through every change before advancing the cursor, so a crash mid-pull can never leave the cursor ahead of the durable replica
Install
@pylonsync/react already includes it; install directly only when you’re using the engine without React.
Construct an engine
start():
- Loads cached entities + cursor from IndexedDB
- Hydrates the mutation queue (offline writes survive restart)
- Resolves the current session via
/api/auth/me - Pulls changes since the last cursor
- Connects the chosen real-time transport
Read the local store
store always reflects the post-merged state — server pushes and optimistic mutations are both applied before listeners fire.
Optimistic mutations
failed in the queue. Subscribe to engine.mutations to surface failures to the UI:
engine.mutations.remove(id). This lets you show “retry” affordances without losing the user’s intent.
Pagination
Hydration
hydrate(...) seeds the local store and the cursor before start() runs, so the first paint is server-rendered and the engine doesn’t waste an initial pull.
Auth integration
The engine reads the bearer token from the storage adapter on every request. To set one:engine.notifySessionChanged() so the engine refreshes /api/auth/me and resets the replica if the visible set changed.
Multi-tenant: switching active org
After calling/api/auth/select-org:
Real-time transport details
WebSocket (primary)
URL is derived frombaseUrl — defaults to ws://host:port+1 for pylon dev (port + 1 is the WS port). Override via wsUrl:
bearer.<percent-encoded-token> Sec-WebSocket-Protocol subprotocol because browser WebSocket has no header API.
Each text frame is a JSON ChangeEvent; binary frames go to whichever consumer registered via engine.onBinaryFrame(handler). The Loro CRDT integration uses this for binary CRDT updates.
Reconnect uses full-jitter exponential backoff — random(0, base * 2^attempts), capped at 30s. The backoff counter resets only after a connection has been stable for 5s, so an auth-failure-then-disconnect loop can’t tight-loop reconnect.
SSE (fallback)
transport: "sse" — connects to http://host:port+2/events (port + 2). Server emits one JSON ChangeEvent per data: line.
Use SSE when:
- The deployment blocks WebSocket (rare, but some corporate proxies do)
- You only need one-way server→client (no presence, no shard inputs)
Polling (last resort)
transport: "poll" — calls /api/sync/pull every pollInterval ms. Use only when SSE and WebSocket are both unavailable.
CRDT subscriptions
For collaborative rows backed by Loro CRDTs:useLoroDoc callers on the same row don’t unsubscribe each other when one unmounts. The engine re-sends active subscriptions on every reconnect so binary frames keep arriving on a fresh socket.
See Loro for the higher-level integration.
Presence + topics
Persistence
The engine writes through to IndexedDB by default in browsers. The schema:entitiesstore — keyedentity:row_id, value{ entity, id, data }cursorsstore — keyedcursor, value{ last_seq }pendingMutationsstore — keyedid, value{ id, change, status, error? }
push() tick.
For non-browser hosts (RN, Tauri, Electron, native Swift), pass a different persistence backend — @pylonsync/react-native ships an Expo SQLite implementation; the Swift SDK has its own.
Resetting
resetReplica() clears the in-memory store, sets the cursor to 0, and persists both. Doesn’t trigger a pull — the caller decides when.