Skip to content

How sync works

Copy page

Nizhal sync is pull-authoritative with push-notify hints. No logical replication, no WAL tailing.

POST /sync/pull returns rows changed since a per-collection cursor (updated_at ordering). The server evaluates sync rules server-side: only rows matching the actor’s bucket scope are included.

Pull responses include:

  • changed — upserts per table
  • tombstoned — soft-deleted row IDs
  • removedBuckets — buckets that left the client’s scope (access revocation)
  • cursor — advance marker for the next pull
  • cursorReset — when the client sent a garbage/future cursor, force full re-bootstrap

Garbage cursors (>24h ahead of wall clock) clamp to 0 and set cursorReset: true so clients do not paginate into a void.

POST /sync/push accepts batches of mutations keyed by clientMutationId. The server:

  1. Claims the mutation (claimMutation) — duplicate delivery is a no-op
  2. Runs the mutator in one transaction
  3. Records applied state + client-id → server-id map (recordApplied)
  4. Publishes bucket pings from the commit chokepoint only

Replaying the entire outbox twice yields byte-identical server state.

Deletes are soft (deleted_at) and tracked in _nizhal_tombstones. Pull includes tombstone IDs so clients remove rows locally. Hard deletes are not synced.

postgresStorage.provision installs:

  • updated_at / deleted_at on synced tables
  • Triggers gated by _nizhal_sync_control (loop-free — mutations do not re-trigger themselves)
  • Indexes for cursor scans

This runs on any Postgres — RDS, Neon, Supabase — without wal_level = logical or replication slots.

nizhalCollectionOptions implements TanStack DB’s SyncConfig:

  1. sync() calls echo.pull with cursor + buckets
  2. begin / write / commit apply rows into the collection
  3. On bucket ping or reconnect, pull again

pull.intervalMs on createNizhalClient is an opt-in fallback interval when realtime is down — the cursor pull still converges (chaos scenario SYNC-5).

Clients should type rows and mutator inputs from GET /nizhal/contract via nizhal gen (planned) — not by importing server Drizzle schema. See The contract.