Quickstart
Copy page
This walkthrough boots a minimal notes app end-to-end: schema, mutators, sync rules, server, and TanStack DB client.
Install
Section titled “Install”pnpm add @nizhal/kernel @nizhal/server @nizhal/db-collection @nizhal/clipnpm add drizzle-orm postgres zodpnpm add @tanstack/react-db @tanstack/db @tanstack/offline-transactionsFor web persistence, add @tanstack/db-wa-sqlite-persistence and wa-sqlite. For React Native, see React Native.
1. Define schema
Section titled “1. Define schema”import { pgTable, text } from "@nizhal/kernel";
export const notes = pgTable("notes", { id: text("id").primaryKey(), owner_id: text("owner_id").notNull(), body: text("body").notNull(),});
export const schema = { notes };2. Define mutators
Section titled “2. Define mutators”import { defineMutator, defineMutators } from "@nizhal/kernel";import { z } from "zod";import { notes } from "./schema";
export const mutators = defineMutators({ addNote: defineMutator( z.object({ id: z.string(), body: z.string() }), async ({ tx, actor, newId }, args) => { const id = args.id || newId(); await tx.insert(notes).values({ id, body: args.body, owner_id: actor.ownerId }); return { serverId: id, affectedBuckets: [`${actor.ownerId}`] }; }, ),});One business operation = one mutator = one server transaction. clientMutationId (from the offline outbox) enables idempotent replay; the server returns client-id → server-id reconciliation when IDs differ.
3. Define sync rules
Section titled “3. Define sync rules”import { defineSyncRules } from "@nizhal/kernel";
export const syncRules = defineSyncRules((b) => ({ myNotes: b.bucket({ parameters: () => b.params({ ownerId: "owner_id" }), data: (bucket) => [ b.table("notes").where(b.eq("owner_id", bucket.ownerId)), ], }),}));defineSyncRules runs a no-leak lint: every data query must constrain rows to bucket keys. Rows outside the rule never reach a client.
4. Provision Postgres
Section titled “4. Provision Postgres”import { postgresStorage } from "@nizhal/server/adapters";import { schema } from "./schema";import { syncRules } from "./sync-rules";
export default { db: process.env.DATABASE_URL, schema, syncRules,};nizhal migrate --config nizhal.config.jspostgresStorage.provision adds updated_at / deleted_at, change-tracking triggers gated by _nizhal_sync_control, tombstone tables, _nizhal_mutations for idempotency, and indexes — no logical replication.
Inspect raw SQL with buildPostgresProvisionPlan from @nizhal/server/adapters if needed.
5. Start the server
Section titled “5. Start the server”import { createNizhalServer, bearerTokenAuth } from "@nizhal/server";
const server = createNizhalServer({ db: process.env.DATABASE_URL!, schema, mutators, syncRules, auth: bearerTokenAuth({ secret: process.env.JWT_SECRET! }),});
server.listen(4000);Endpoints:
POST /sync/pull— cursor-based deltaPOST /sync/push— idempotent mutator batchGET /sync/stream— WebSocket bucket pingsGET /nizhal/contract— OpenAPI/JSON-Schema artifact
6. Wire the client
Section titled “6. Wire the client”import { createNizhalClient, nizhalCollectionOptions, createNizhalMutators } from "@nizhal/db-collection";import { createCollection } from "@tanstack/react-db";
const echo = createNizhalClient({ server: "http://localhost:4000", auth: { getHeaders: () => ({ Authorization: `Bearer ${token}` }), }, bucketsForSyncRule: (rule) => { if (rule === "myNotes") return [{ ownerId: session.ownerId }]; return []; },});
const notesCollection = createCollection( nizhalCollectionOptions({ name: "notes", syncRule: "myNotes", echo }),);
const { mutate } = createNizhalMutators({ collections: { notes: notesCollection }, echo, mutators });
// Optimistic local write; outbox drains to /sync/push when onlineawait mutate.addNote({ id: crypto.randomUUID(), body: "Hello offline" });On connect (and on each bucket ping), the collection pulls changes since its cursor. After reconnect, devices converge without a full refetch.
7. Issue a token
Section titled “7. Issue a token”import { issueBearerToken } from "@nizhal/server";
const token = issueBearerToken({ userId: "user-1", ownerId: "owner-1", secret: process.env.JWT_SECRET!,});Verify
Section titled “Verify”Run the server, open two browser tabs (or use apps/credit-ledger/examples/live-e2e.ts as a reference for a scripted check). Write offline in one tab, reconnect, and confirm the other tab converges via pull.
- First app — movement-ledger domain modeling
- How sync works — cursors, tombstones, idempotency