Skip to content

Build an offline-first app

Copy page

Confirm offline writes are a hard requirement — not speculative. See Introduction.

Prefer append-only facts and derived folds over mutable aggregates. If you need shared counters, use ledger entries or server-side mutator invariants.

Declare tables with @nizhal/kernel Drizzle re-exports. Set merge: "field" or CRDT columns only where LWW is insufficient.

Every user action maps to exactly one mutator. Server fn enforces invariants; return { serverId, affectedBuckets } when client IDs reconcile.

Write rules that scope every synced table to actor buckets. Run tests that assert assertSyncRulesNoLeak passes. Use b.membership for dynamic tenancy.

nizhal migrate against your Postgres (local or managed). Commit the provision plan in CI for drift detection if needed.

createNizhalServer with bearerTokenAuth, postgresStorage, and inProcessRealtime (single instance) or listenNotifyRealtime (multi-instance). See Self-hosting.

One nizhalCollectionOptions per synced table/sync-rule pair. Wire createNizhalMutators once; UI calls mutate.*.

Enable waSqlitePersistence or opSqlitePersistence before testing offline — otherwise refresh loses the outbox.

Run pnpm chaos emulation scenarios against your domain or extend apps/emulation. See Production validation.

  • Storing mutable balance without ledger backing
  • Raw SQL in sync-rule data queries
  • Importing server Drizzle types on the client
  • Relying on WS payloads for row data (pull is authoritative)