Skip to content

Public surface for the four @nizhal/* packages. Types are generated from GET /nizhal/contract via nizhal gen (planned); do not import server Drizzle schema on the client.

Schema, mutators, sync rules, and contract emission.

Define tables with Drizzle pgTable re-exports from @nizhal/kernel. Set per-table merge policy via a table source object:

import { pgTable, text, crdtText, crdtMap } from "@nizhal/kernel";
// Default: last-write-wins (commit-order on server)
const notes = pgTable("notes", { id: text("id").primaryKey(), body: text("body") });
// Field-level merge: each column resolves independently (HLC-ordered scalars)
const docs = {
table: pgTable("docs", { id: text("id").primaryKey(), title: text("title") }),
merge: "field" as const,
};
// CRDT columns (Yjs-backed map/text) inside a field-merge table
const collab = {
table: pgTable("collab_docs", {
id: text("id").primaryKey(),
body: crdtText("body"),
meta: crdtMap("meta"),
}),
merge: "field" as const,
};
mergeBehavior
"lww"Whole-row last-write-wins by server commit order (default)
"field"Per-column merge; scalars use HLC, CRDT columns merge structurally
"crdt"Column-level CRDT mode via crdtText / crdtMap

Helpers: crdtText, crdtMap, schemaMergeMode, schemaMergePolicy, emitNizhalContract.

import { defineMutator, defineMutators } from "@nizhal/kernel";
import { z } from "zod";
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 enables idempotent replay; server returns client-id → server-id reconciliation.
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 is no-leak-linted: predicates must scope every synced row to the actor’s buckets.

Builder methods: b.bucket, b.params, b.membership, b.table, b.eq, b.raw (parameters only).

emitNizhalContract({ schema, mutators, syncRules }) produces the OpenAPI/JSON-Schema artifact served at GET /nizhal/contract.


Hono sync server, storage, realtime, auth, jobs, and observability.

import { createNizhalServer } from "@nizhal/server";
const server = createNizhalServer({
db: process.env.DATABASE_URL!,
schema,
mutators,
syncRules,
auth,
storage, // optional; defaults to postgresStorage(db)
realtime, // optional; defaults to inProcessRealtime()
blob, // optional BlobAdapter
observer, // optional NizhalObserver hooks
jobs, // optional durable job registry
limits, // body size + per-actor rate limit
presence, // heartbeat timeout
});
server.listen(4000);

Endpoints: POST /sync/pull, POST /sync/push, GET /sync/stream (WebSocket), GET /nizhal/contract, GET /nizhal/stats (admin).

import { postgresStorage } from "@nizhal/server/adapters";
const storage = postgresStorage({ connectionString: process.env.DATABASE_URL! });

StorageAdapter interface (implemented by postgresStorage):

  • getChanges — cursor pull with bucket scope, tombstones, removedBuckets
  • transaction — atomic mutator execution
  • claimMutation / recordApplied — idempotent push + client-id reconciliation
  • provision — no-WAL DDL (columns, triggers, indexes) from schema + sync rules

buildPostgresProvisionPlan returns the raw SQL statements for inspection.

AdapterImportUse case
inProcessRealtime@nizhal/server/adaptersDefault single-process pub/sub from commit chokepoint
listenNotifyRealtime@nizhal/server/adaptersMulti-instance Postgres via LISTEN/NOTIFY
cloudflareRealtime@nizhal/server/adapters/cloudflareCloudflare Workers + Durable Objects (PartyServer)
cloudflareHttpRealtime@nizhal/server/adapters/cloudflareHTTP long-poll fallback on Workers

All implement RealtimeAdapter: publish(bucket) (commit chokepoint only) + subscribe(onPing).

Presence v2: track / untrack / heartbeat on /sync/stream; state and diffs are bucket-scoped.

import { bearerTokenAuth, issueBearerToken } from "@nizhal/server";
const auth = bearerTokenAuth({ secret: process.env.JWT_SECRET! });
const token = issueBearerToken({ userId, ownerId, secret });

NizhalAuth.resolve(req) returns { userId, ownerId } or null.

import { localFsBlobStore, s3BlobStore, r2BlobStore, blobDb } from "@nizhal/server/adapters";

Presigned upload/download URLs; reference rows sync via blobDb(storage).

Hooks: onPull, onPush, onConflict, onError. gatherStats(db, realtime) powers GET /nizhal/stats.

createJobScheduler + createJobWorker for durable background tasks enqueued from mutators.


TanStack DB SyncConfig adapter, offline mutators, presence, persistence.

import { createNizhalClient } from "@nizhal/db-collection";
const echo = createNizhalClient({
server: "http://localhost:4000",
auth: { getHeaders: () => ({ Authorization: `Bearer ${token}` }), refresh },
bucketsForSyncRule: (rule) => [...],
subscribeSource, // optional; defaults to PartySocket
reconnect, // jitter + catch-up on reconnect
ttl, // evict out-of-scope bucket rows locally
pull, // intervalMs + page size for bootstrap
presence, // heartbeat interval
status, // SyncStatus + outbox inspection
});

Methods: pull, push, subscribe, track/untrack, presenceState, onPresence, cursor/scope helpers.

Returns a TanStack DB CollectionConfig wired to Nizhal pull/push:

import { nizhalCollectionOptions } from "@nizhal/db-collection";
const notesConfig = nizhalCollectionOptions({
name: "notes",
syncRule: "myNotes",
echo,
persistence: waSqlitePersistence({ ... }), // optional
});

Wraps kernel mutators with offline-durable optimistic writes (TanStack offline-transactions). Poison failures quarantine without wedging the outbox.

track, untrack, presenceState, onPresence, subscribePresence, presence — v2 metas-per-key with join/leave diffs.

ExportPlatform
waSqlitePersistenceWeb (wa-sqlite + OPFS)
opSqlitePersistenceReact Native (op-sqlite)
migrateClientStoreClient-store schema migrations

createCrdtText, createCrdtMap, applyCrdtUpdate, encodeCrdtUpdate — client-side Yjs editing before push.

createNizhalBlobs, memoryBlobStore, keyForBlob — client blob upload/download. createNizhalStatus — exposes sync status and poison-quarantine outbox entries.

createPartySocketSource, createCloudflareSubscribeSource — custom realtime transports.


nitroWebSocketSource, installNitroFetch, nitroFetch, createNizhalNitroClient, reactNativeOnlineDetector, installNizhalNativePolyfills


Terminal window
nizhal migrate # provision no-WAL DDL from schema + sync rules
nizhal gen # (planned) generate client types from /nizhal/contract
nizhal introspect # (planned) brownfield schema introspection

NizhalMigrateConfig: { db, schema, syncRules, storage? }.


All four libraries ship at 0.1.0 with publishConfig.access: "public" and files: ["dist"] (no source maps in tarballs).