Valet

How Sync Works

Subscriptions, deltas, and the sync protocol.

Client
Subscribe (query, args)
evaluate query
SubscribeResponse (data)
register subscription
... time passes ...
data changes
Sync (deltas)
Server

When you call useQuery, the client subscribes to a query on the server. The server sends the initial data, then pushes incremental deltas whenever the underlying documents change.

The subscription lifecycle

  1. Your component calls useQuery with a query name and arguments
  2. The client sends a Subscribe message over the WebSocket (query name, args, last known sync version)
  3. The server evaluates the query
  4. The server responds with a SubscribeResponse containing the initial data and a sync version
  5. The data writes to the client's local SQLite database (if SyncEngine is configured)
  6. React renders with the data
  7. The server registers the subscription for ongoing change detection
  8. When documents matching the query change on the server, it detects which subscriptions are affected
  9. The server sends a Sync message with deltas (insert, update, or remove)
  10. The client applies the deltas to local SQLite and React re-renders

Initial sync vs delta sync

On the first subscription, the server sends all matching documents as a full sync. The client stores them in local SQLite and records the sync version.

On reconnect, if the client sends a last_sync_version, the server sends only what changed since that version as a delta sync. This makes reconnection fast even with large datasets -- the client already has most of the data.

Delta operations

Each Sync message contains one or more deltas. Each delta describes a change to a single document:

OperationMeaning
insertA document now matches the subscription's sync filter
updateA document that was already in the subscription changed
removeA document no longer matches the sync filter (deleted or filtered out)
replaceFull dataset replacement (used for server-execution queries)

Sync protocol

Messages are JSON over WebSocket. The current protocol version is PROTOCOL_VERSION = 4.

MessageDirectionPurpose
HelloServer to ClientSends protocol version on connect
AuthClient to ServerSends JWT token for authentication
AuthOkServer to ClientAuthentication succeeded (includes user ID, token expiry)
AuthErrorServer to ClientAuthentication failed (includes error code)
SubscribeClient to ServerStarts a subscription (query name, args, last sync version)
SubscribeResponseServer to ClientInitial data for a subscription
SyncServer to ClientDelta updates for active subscriptions
SyncWriteClient to ServerSends a mutation (handler name, args, captured environment)
SyncWriteResultServer to ClientMutation confirmation or error
UnsubscribeClient to ServerEnds a subscription

Connection flow

  1. Client opens a WebSocket connection
  2. Server sends Hello with its protocol version
  3. Client sends Auth with a JWT token (if authentication is configured)
  4. Server responds with AuthOk or AuthError
  5. Client sends Subscribe messages for each active query
  6. Server responds with SubscribeResponse for each subscription
  7. Ongoing: server sends Sync messages as data changes
  8. Client sends SyncWrite messages for mutations

Sync filters and membership

Each subscription has a sync filter that determines which documents belong to it. When a document changes, the server evaluates it against all active subscriptions for that table:

  • Document now matches but was not a member -- the server sends an insert delta
  • Document still matches and was a member -- the server sends an update delta
  • Document no longer matches but was a member -- the server sends a remove delta

Sync filters are defined in your schema with the .sync() method on a table. They control which documents sync to which clients. For example, a sync filter of q.eq('userId', ctx.auth.userId) ensures each client receives only its own documents.

Conflict handling

Valet handles conflicts through two complementary mechanisms:

Rebase for offline mutations

When a client reconnects after being offline, Valet uses a rebase model for conflict resolution. Offline mutations replay against fresh server state on reconnect. Each mutation handler re-executes against current data, so read-then-write patterns (like incrementing a counter) produce correct results.

The rebase model addresses the offline case, where a client's mutations may be based on stale data. See Local-First Architecture for the full explanation of how rebase and deterministic replay work.

Optimistic locking for concurrent online writes

When multiple online clients attempt to update the same document concurrently, Valet uses optimistic locking via the _version field to detect conflicts. Each document has a _version that increments on every update. If a client attempts to update a document with a stale _version, the server returns a VERSION_CONFLICT error and the operation fails. The client must handle this error and retry with fresh data.

This prevents lost updates when multiple clients modify the same document simultaneously. Unlike rebase (which resolves conflicts by replaying operations), optimistic locking prevents conflicts by rejecting stale writes.

At the storage level, concurrent writes to different fields resolve via last-write-wins. Optimistic locking applies when clients explicitly specify a version in their write operations.

On this page