Skip to main content

Schema

import * as mod from "jsr:@nzip/lofi/schema";

Declare application tables, permissions, and migrations without importing Jazz directly.

This module is a curated re-export of the pinned Jazz 2 schema DSL: the same names and semantics, narrowed to the surface lofi documents and tests. Author code imports s from here instead of jazz-tools, so version bumps and upstream renames are absorbed by the package rather than by every application schema.

import { s } from "@nzip/lofi/schema";

export const app = s.defineApp({
tasks: s.table({
text: s.string(),
completed: s.boolean(),
createdAt: s.timestamp(),
}),
});

Shared-field columns (sharedEncryptedText, sharedEncryptedJson) declare only the sealed data shape here; the policy and key-lifecycle half — group templates, key bootstrap and rotation, member reconciliation — lives in @nzip/lofi/access, and pin remediation for changed peer keys is surfaced through the main @nzip/lofi entry.

chain

function

function chain<Row extends { id: string }, NextInput, NextResult>(next: (input: NextInput) => WriteHandle<NextResult>, toInput: (row: EffectRow<Row>) => NextInput): EffectUnit<Row>

Data-internal unit: issues a follow-up verb once the write syncs, mapping the settled row to the next verb's input. Reifies a chain of writes (reserve → charge → fulfill) declaratively, without a saga API — each link is an ordinary verb, so its own effects and rejection handling apply. Fires only on synced; a rejected write starts no chain.

The follow-up is journaled under a deterministic child write id derived from this obligation. Its record remains retained until the parent commits delivery, so a crash between those steps reuses the child instead of issuing the mutation again.

Example

export const reserve = s.mutation("reserve", s.insert(app.holds), {
effects: [s.chain(charge, (hold) => ({ holdId: hold.id, amount: hold.total }))],
});

debug

function

function debug(): EffectUnit<{ id: string }>

Observation unit: a development-only timeline entry for each fate an obligation settles, for eyeballing effect delivery in the inspector. Stripped from production builds — in a PROD bundle the handlers record nothing, so it costs a closure and no more.

Example

export const editNote = s.mutation("editNote", s.update(app.notes), {
effects: [s.debug()],
});

DefinedTable

class

class DefinedTable<TColumns extends TableDefinition = TableDefinition> {
constructor(columns: TColumns, indexedColumns?: readonly Extract<keyof TColumns, string>[] | undefined);
readonly columns: TColumns;
readonly indexedColumns?: readonly Extract<keyof TColumns, string>[] | undefined;
readonly __jazzTableDefinition: true;
indexOnly<TColumnsForIndex extends readonly [Extract<keyof TColumns, string>, ...Extract<keyof TColumns, string>[]]>(columns: TColumnsForIndex): DefinedTable<TColumns>;
}

defineNestedApp

function

function defineNestedApp<TDef extends NestedSchemaDefinition>(definition: TDef): NestedApp<TDef> & NestedAppRoot

Declares nested app namespaces over one compiled schema. Each namespace's tables become ordinary typed table handles under their unprefixed names:

export const root = s.defineNestedApp({
taskapp: {
tasks: s.table({ text: s.string(), completed: s.boolean() }),
},
notesapp: {
notes: s.table({ title: s.string() }),
},
});
// root.taskapp.tasks and root.notesapp.notes are ordinary table handles.

Handles are constructed exactly once, here — the runtime keys table stores by handle identity, so this is the only expressible construction shape and duplicate subscriptions cannot arise. Declare permissions per namespace with defineNestedPermissions and combine the compiled bundles with mergeNestedPermissions.

Known constraint (alpha.53): every table is a runtime sibling of every namespace, so the g-set isolation guidance cannot be satisfied inside one store — keep g-set columns out of nested apps until the upstream destabilization pin clears (see the conformance findings in docs/decisions/schema-facade-alpha53.md).

defineNestedPermissions

function

function defineNestedPermissions<TApp extends object>(namespace: TApp, factory: (ctx: PolicyContext<TApp>) => void): CompiledPermissions

Declares permissions for one namespace of a nested app. The policy context exposes only that namespace's tables, under their unprefixed local names; the compiled output is a flat record keyed by the mangled global table names, so per-namespace bundles merge collision-free into the one deployed bundle:

export const taskPermissions = s.defineNestedPermissions(
root.taskapp,
({ policy }) => {
policy.tasks.allowRead.always();
},
);

Use this — not s.definePermissions — for nested namespaces: the pinned definePermissions keys compiled rules by the app object's property names, so calling it on a namespace directly would compile unprefixed table names that the deployed schema does not contain.

effect

function

function effect<T extends { id: string }, Init>(name: string, table: TableProxy<T, Init>, handlers: EffectHandlers<T>, options: EffectUnitOptions): EffectUnit<T>

Declares a named, reusable, typed effect unit over one table.

The name is durable: the journal re-arms this unit's handlers by name after a reload, so renaming a unit orphans obligations journaled under the old name. Names must be unique per app; a duplicate declaration throws.

Handlers run once, on the originating device, with at-least-once delivery: a crash between handler start and journal completion re-runs the handler at the next boot, so handlers that call external services should pass EffectContext.journalId as the idempotency key.

Example

const chargeCard = s.effect("chargeCard", app.schema.orders, {
onSynced: async (order, { journalId }) => {
// The world accepted the order. journalId is the idempotency key.
},
onRejected: async (order) => {
// The order never happened: compensate what the user saw.
},
});

EncryptedColumnError

class

class EncryptedColumnError extends Error {
constructor(code: "key-missing" | "key-invalid" | "corrupt", message: string);
readonly name: string;
readonly code: "key-missing" | "key-invalid" | "corrupt";
}

Raised when an encrypted column cannot seal or open a value.

encryptedColumnsOf

function

function encryptedColumnsOf(tableName: string): ReadonlyMap<string, string> | undefined

The registered encrypted columns of a table, keyed by column name.

encryptedDate

function

function encryptedDate(label: string): EncryptedColumn<Date>

A date column sealed on the client before it enters Jazz; the view type is Date, matching the plain timestamp column. Same label semantics as encryptedText. Invalid dates are rejected at write time.

encryptedJson

function

function encryptedJson<T = unknown>(label: string): EncryptedColumn<T>

A JSON-value column sealed on the client before it enters Jazz; the view type is the parsed value. Same label semantics as encryptedText.

encryptedNumber

function

function encryptedNumber(label: string): EncryptedColumn<number>

A number column sealed on the client before it enters Jazz; the view type is number. Same label semantics as encryptedText. The stored representation is text, so values beyond the plain integer column's 32-bit range round-trip exactly up to double precision. Non-finite values are rejected at write time.

encryptedText

function

function encryptedText(label: string): EncryptedColumn<string>

A text column sealed on the client before it enters Jazz. label is the column's stable identity, conventionally "table.column"; it domain- separates the subkey and is bound as associated data, so changing it later makes existing values unreadable — treat it like a column name.

notes: s.table({
title: s.string(),
body: s.encryptedText("notes.body"),
}),

flattenNestedSchema

function

function flattenNestedSchema(definition: NestedSchemaDefinition): SchemaDefinition

Flattens a nested schema definition into the single global table namespace that actually compiles and deploys: taskapp.tasks becomes taskapp__tasks. Ref targets written against namespace-local names (or as "<namespace>.<table>" across namespaces) are rewritten to the mangled global names. Use the flattened definitions as the from/to schemas when authoring migrations over a nested app; moving a table between namespaces is then an ordinary renameTableFrom migration.

insert

function

function insert<T, Init>(table: TableProxy<T, Init>): MutationOp<T, Init, "insert">

Declares that a verb inserts rows into table. A mutation declared over this op is called as (values) => WriteHandle<Row>; await resolves at saved with the created row.

isEncryptedColumn

function

function isEncryptedColumn(tableName: string, columnName: string): boolean

Whether the named column of the named table is an encrypted column.

isPermanentEffectError

function

function isPermanentEffectError(error: unknown): error is PermanentEffectError

True when a handler failure asked to retire rather than retry.

isSharedFieldError

function

function isSharedFieldError(error: unknown): error is SharedFieldError

True when an error came from the lofi shared-field surface.

log

function

function log(label: string): EffectUnit<{ id: string }>

A built-in effect unit that records a structured entry in runtime diagnostics on either fate. Repeated calls with one label return the same unit, so a label can be shared by several verbs.

Example

export const addTask = s.mutation("addTask", s.insert(app.schema.tasks), {
effects: [s.log("task-writes")],
});
// Each addTask write records a "task-writes" diagnostics entry when it
// syncs or is rejected.

mark

function

function mark<T extends { id: string }, Init>(table: TableProxy<T, Init>, config: MarkConfig<Init>): EffectUnit<T>

Data-internal unit: patches the written row when its fate resolves, so write fate becomes replicated data every device and query sees — the hand-rolled status column made declarative. The patch is absolute (a static set-column-to-value object), so it is convergent under re-delivery: applying it twice lands the same row.

A rejected insert has no row to mark — the engine rolled it out — so the rejected patch is skipped for inserts; on updates and removes the row survives the rollback and the patch records the failure.

The mark is best-effort: its patch is an ordinary update that the store may itself deny (a policy that governs the status column), or that finds no row (an update whose row was concurrently removed elsewhere). In those cases the mark simply does not land — it is a convenience over the manual status column, not a delivery guarantee. For a fate signal that must survive, pair it with notice (durable queue) or a webhook.

Example

export const submit = s.mutation("submit", s.update(app.claims), {
effects: [s.mark(app.claims, {
synced: { status: "confirmed" },
rejected: { status: "failed" },
})],
});

matchDecrypted

function

function matchDecrypted<T>(rows: readonly T[], predicate: (row: T) => boolean): T[]

Filters live-query rows on decrypted values, client-side. Encrypted columns cannot appear in where — the store holds ciphertext — but rows reaching the caller are already decrypted, so arbitrary predicates run locally: narrow the query with plaintext filters first, then match here, and only then apply any limit (a limit() before the predicate under-fetches). Sorting on an encrypted column is the same pattern: sort the returned rows, never orderBy the column (ciphertext order is arbitrary).

mergeNestedPermissions

function

function mergeNestedPermissions(...bundles: readonly CompiledPermissions[]): CompiledPermissions

Combines per-namespace compiled permission bundles into the one bundle a nested app deploys. Namespaced table names cannot collide by construction; a duplicate key means the same namespace was compiled twice and throws.

mutation

function

function mutation<T extends { id: string }, Init, Kind extends MutationOpKind>(name: string, op: MutationOp<T, Init, Kind>, options: MutationOptions<T>): MutationVerb<MutationOp<T, Init, Kind>>

Declares a typed, callable verb: a named mutation over one table operation, carrying its effect units. Call sites invoke the verb like a function and receive a WriteHandle; await resolves at saved.

Verb names are app-unique and durable — the journal attributes re-armed obligations through them — and read as imperative verb phrases (placeOrder, not orderInsert). Inline onSynced/onRejected are sugar for an implicit single unit named after the verb.

Example

export const placeOrder = s.mutation("placeOrder", s.insert(app.schema.orders), {
effects: [chargeCard, s.log("order-placed")],
});

const write = placeOrder({ item, qty }); // WriteHandle<Order>
await write; // saved: durable on this device
await write.synced; // confirmed by the store

NESTED_SEPARATOR

const

const NESTED_SEPARATOR: "__";

The reserved separator between a namespace and a table in the flattened global table name. It is deliberately not .: the permission builder reserves ${string}.${string} keys for qualified-column where entries. Namespace and table names must not contain it.

nestedAppDeployTarget

function

function nestedAppDeployTarget(app: NestedAppRoot): object

An app-like value over every table of a nested app root from defineNestedApp, keyed by the mangled global table names — the shape schema deploy tooling and the policy test harness expect. As runtime defense for callers that cast past the NestedAppRoot requirement, a value without the root marker throws.

nestedAppTables

function

function nestedAppTables(app: NestedAppRoot): readonly object[] | null

The flattened table-handle registry of a nested app root from defineNestedApp. The runtime consumes this so nested tables participate in boot readiness and local-to-managed row migration; the handles are the same objects the namespaces expose, so store identity is preserved. As runtime defense for callers that cast past the NestedAppRoot requirement, a value without the root marker returns null.

notice

function

function notice<Row extends { id: string } = { id: string }>(config: NoticeConfig<Row>): EffectUnit<Row>

Data-internal unit: enqueues a durable, user-visible message when the write settles — the fix for the "a rejected write still flashed success" failure mode. The queue is durable and UI-agnostic: an entry created at a boot re-arm survives with nothing mounted, and a component (the built-in notices surface, or an author's) renders it. Toasts are a userland wrapper over the queue, never an imperative call from here.

Idempotent by the obligation's journal id: a re-delivered handler enqueues the same keyed entry once, so a crash-and-replay shows one message.

Example

export const publish = s.mutation("publish", s.update(app.posts), {
effects: [s.notice<Post>({
synced: "Published.",
rejected: (post) => `Could not publish "${post.title}".`,
})],
});

PermanentEffectError

class

class PermanentEffectError extends Error {
constructor(message: string);
readonly name: string;
}

Thrown from an effect handler to retire the obligation immediately instead of re-arming it. Delivery is at-least-once by default: an ordinary thrown error is retryable — the handler re-runs at the next boot until it succeeds or EffectUnitOptions.maxAttempts quarantines it. Some failures are known to be permanent — a webhook receiver answered 400, a row a handler needed is gone for good — and retrying only burns attempts and delays the quarantine diagnostic. Throwing this retires the obligation now, counted as a permanent handler failure. The message reaches diagnostics.

Example

s.effect("charge", app.orders, {
onSynced: async (order, { journalId }) => {
const res = await fetch(url, { headers: { "Idempotency-Key": journalId } });
if (res.status >= 400 && res.status < 500) {
throw new PermanentEffectError(`charge refused: ${res.status}`);
}
if (!res.ok) throw new Error(`charge transient failure: ${res.status}`);
},
});

plain

function

function plain<T extends object>(column: T): PlainColumn<T>

Marks a column of a privateTable as deliberately plaintext — because it is a filter, sort key, or permission target the server must evaluate. The marker is the visible record of that decision at the column it affects.

privateTable

function

function privateTable<TCols extends Record<string, AnyColumnBuilder>>(labelPrefix: string, columns: TCols): DefinedTable<PrivateTableColumns<TCols> extends TableDefinition ? PrivateTableColumns<TCols> : TableDefinition>

Declares a table whose columns are sealed by default. labelPrefix is the cryptographic identity prefix of every sealed column ("prefix.column") — treat it like the table name and never reuse it across tables. Columns opt out of sealing only by being reference columns, byte columns (reported), or wrapped in plain; optionals, defaults, non-lww merge strategies, and transforms on sealed columns are configuration errors.

remove

function

function remove<T, Init>(table: TableProxy<T, Init>): MutationOp<T, Init, "remove">

Declares that a verb removes rows from table. A mutation declared over this op is called as (id) => WriteHandle<void>; await resolves at saved.

s

const

const s: SchemaDsl;

The lofi schema surface. Use in src/schema.ts and src/permissions.ts in place of a raw jazz-tools import; Jazz member names and behavior are identical to the pinned Jazz 2 DSL. The lofi-owned members: nested namespaces (defineNestedApp), sealed columns (encryptedText, encryptedJson, encryptedNumber, encryptedDate, with privateTable and plain for encrypt-by-default tables), and the verb grammar — mutation declares callable verbs over insert, update, and remove, carrying effect units and log entries. Verb calls return a WriteHandle, observed in UI through useWrite and usePendingWrites from @nzip/lofi/preact.

sharedEncryptedJson

function

function sharedEncryptedJson<T = unknown>(label: string, options: SharedColumnOptions): EncryptedColumn<SharedFieldValue<T>>

A JSON-value column sealed under a group field key; the ready state holds the parsed value. Same label and option semantics as sharedEncryptedText.

sharedEncryptedText

function

function sharedEncryptedText(label: string, options: SharedColumnOptions): EncryptedColumn<SharedFieldValue<string>>

A text column sealed under a group field key: every member holding the key reads it; the store operator never does. label is the column's cryptographic identity ("table.column"), and the options name the group table, the sibling column referencing the group, the wrapped-key table, and the key directory.

docs: s.table({
workspaceId: s.ref("workspaces"),
body: s.sharedEncryptedText("docs.body", {
group: "workspaces",
groupIdColumn: "workspaceId",
keys: "workspaceFieldKeys",
directory: "keyDirectory",
}),
}),

SharedFieldError

class

class SharedFieldError extends Error {
constructor(code: SharedFieldErrorCode, message: string);
readonly name: string;
readonly code: SharedFieldErrorCode;
}

Raised when shared-field material cannot be derived, wrapped, or opened.

sharedFieldReady

function

function sharedFieldReady<T>(value: SharedFieldValue<T>): value is { state: "ready"; value: T }

Whether a shared field value holds decrypted content.

trace

function

function trace(label?: string): EffectUnit<{ id: string }>

Observation unit: a span from the write's journaling to its settled fate, recorded in runtime diagnostics as an OpenTelemetry-shaped event with the saved→synced/rejected latency. Pure instrumentation — it changes no state and cannot fail a write. The optional label groups related spans; without one the verb name labels the span. Repeated calls with one label share a unit, so a label can be reused across verbs.

Example

export const placeOrder = s.mutation("placeOrder", s.insert(app.orders), {
effects: [s.trace("checkout")],
});

unwrapSharedField

function

function unwrapSharedField<T>(value: SharedFieldValue<T>): T

The decrypted content of a shared field, for callers who prefer an exception over a state check. Throws key-pending while the wrap is in flight and corrupt for failed authentication.

update

function

function update<T, Init>(table: TableProxy<T, Init>): MutationOp<T, Init, "update">

Declares that a verb updates rows of table. A mutation declared over this op is called as (id, patch) => WriteHandle<void>; await resolves at saved.

webhook

function

function webhook(name: string, url: string, options: WebhookOptions): EffectUnit<{ id: string }>

External unit: POSTs the settled row and its fate to url, using name as its safe durable identity and with the obligation's journal id auto-injected as Idempotency-Key so a re-delivery the receiver already saw is dropped receiver-side. The generic outside-world workhorse and the reference for the at-least-once contract.

Failure severity follows the response: a transient failure (network error, 5xx, 429) throws an ordinary error, so the ledger's bounded backoff retries it; a 4xx (other than 429) throws PermanentEffectError, retiring the obligation without burning the retry budget on a request the receiver will keep refusing. Because receiver keys expire, delivery defaults to a 24-hour window (WebhookOptions.expiresAfterMs).

Example

export const order = s.mutation("order", s.insert(app.orders), {
effects: [s.webhook("orders", "https://hooks.example.com/orders")],
});

WriteHandle

class

class WriteHandle<T> implements PromiseLike<T> {
constructor(writeId: string);
get writeId(): string;
get batchId(): string | null;
get stage(): WriteStage;
get reason(): WriteRejection | null;
get saved(): Promise<T>;
get synced(): Promise<T>;
then<Fulfilled = T, Rejected = never>(onfulfilled?: ((value: T) => Fulfilled | PromiseLike<Fulfilled>) | null, onrejected?: ((reason: unknown) => Rejected | PromiseLike<Rejected>) | null): Promise<Fulfilled | Rejected>;
subscribe(listener: () => void): () => void;
}

A single write observed through the author-facing lifecycle.

await write (the thenable) resolves at saved with the write's value — for inserts, the created row. write.synced resolves when the store confirms the write and rejects with WriteRejectedError when the store denies it. stage and reason are current-state properties; subscribe notifies immediately and on every later change.

On a device without managed sync there is no store to confirm anything: local durability is settlement, and the handle reaches synced as soon as it is saved. In Preact components, render a handle with useWrite and the app-wide pending set with usePendingWrites.

Handles are issued by the runtime and are observe-only: the lifecycle mutators live on a controller the ledger keeps at construction, so no consumer of a handle can advance or settle it.

Example

const write = placeOrder({ sku, quantity }); // a verb returns a WriteHandle
const order = await write; // resolves at saved — safe to navigate
write.synced.catch((error) => {
if (error instanceof WriteRejectedError) showDenied(error.message);
});

AnyColumnBuilder

type

type AnyColumnBuilder = TypedColumnBuilder<SqlType, boolean, string | undefined, boolean, unknown>;

Any pinned-Jazz column builder accepted as a privateTable column.

App

type

type App<TSchema extends SchemaLike> = Simplify<{ [mapped type] } & { wasmSchema: WasmSchema; union<TTable extends string>(relations: readonly RelationSeedQuery<TTable>[]): TypedTableQueryBuilder<any, any, any, any> }>;

ArrayColumn

type

type ArrayColumn<ElementSql extends SqlType = SqlType, Optional extends boolean = false, Ref extends string | undefined = undefined, HasDefault extends boolean = false, Value = TSTypeFromSqlType<{ kind: "ARRAY"; element: ElementSql }>> = TypedColumnBuilder<{ kind: "ARRAY"; element: ElementSql }, Optional, Ref, HasDefault, Value>;

BooleanColumn

type

type BooleanColumn<Optional extends boolean = false, HasDefault extends boolean = false, Value = boolean> = TypedColumnBuilder<"BOOLEAN", Optional, undefined, HasDefault, Value>;

BytesColumn

type

type BytesColumn<Optional extends boolean = false, HasDefault extends boolean = false, Value = Uint8Array> = TypedColumnBuilder<"BYTEA", Optional, undefined, HasDefault, Value>;

CompiledPermissions

type

type CompiledPermissions = Record<string, TablePolicies>;

EffectContext

type

type EffectContext = {
journalId: string;
writeId: string;
verb: string | null;
table: string;
op: "insert" | "update" | "remove";
rowId: string;
writeCreatedAt: number;
fate: "synced" | "rejected";
cause: "denied" | "expired" | null;
code: string | null;
reason: string | null;
};

Delivery metadata passed to every effect handler. Delivery is at-least-once: a crash between handler start and journal completion re-runs the handler at the next boot, so handlers calling external services should pass EffectContext.journalId as an idempotency key.

EffectHandlers

type

type EffectHandlers<Row> = {
onSynced?: (row: EffectRow<Row>, context: EffectContext) => void | Promise<void>;
onRejected?: (row: EffectRow<Row>, context: EffectContext) => void | Promise<void>;
};

The action and compensation handlers one effect unit pairs.

EffectRow

type

type EffectRow<Row> = Partial<Row> & { id: string };

The row an effect handler receives. In the session that performed the write it is the write's snapshot: the full row for inserts, the changed columns for updates, only the id for removes. After a reload the journal holds no column values: a synced handler receives the row fetched live from the store — the final merged state — and a rejected handler receives the id alone, because the engine rolled the row back and identity plus EffectContext.cause is all that remains. Treat every column except id as optional.

EffectUnit

type

type EffectUnit<Row = { id: string }> = {
readonly effectName: string;
readonly handlers: EffectHandlers<Row>;
readonly expiresAfterMs?: number | null;
readonly maxAttempts?: number;
readonly anonymousPrefix?: string;
};

A named, reusable pairing of action and compensation. The name is the durable identity the journal uses to re-arm handlers after a reload; a mutation declares its units once, at the verb declaration.

EffectUnitOptions

type

type EffectUnitOptions = {
expiresAfterMs?: number;
maxAttempts?: number;
};

Retention options for one effect unit: how long delivery may lag the write, and how many failing attempts are made before quarantine.

EncryptedColumn

interface

interface EncryptedColumn<TView> extends Omit<TypedColumnBuilder<EncryptedStoredSql, false, undefined, false, TView>, "default" | "merge" | "transform" | "optional"> {
readonly encryptedColumnBrand?: TView;
default(value: never): never;
merge(strategy: never): never;
transform(transform: never): never;
optional(this: never): never;
}

The column type of every s.encrypted* constructor. Structurally a valid table column with the declared view type, but excluded from where filters at compile time — the store holds ciphertext, so a filter could only ever compare sealed bytes. The chaining modifiers are disabled: a .default() would be applied below the seal boundary as plaintext, .merge() semantics other than last-write-wins cannot operate on ciphertext, .transform() would replace the seal itself, and .optional() stays disabled until the engine's null handling of transformed columns is pinned.

EncryptedStoredSql

type

type EncryptedStoredSql = "TEXT" | "BYTEA";

The phantom stored SQL type of encrypted columns. The union is deliberate: the engine's where-input mapping branches on a single stored sql type, so a union matches no branch and every filter position for the column collapses to never. Row and insert types read the view type and are unaffected.

EnumColumn

type

type EnumColumn<Variants extends readonly string[] = readonly string[], Optional extends boolean = false, HasDefault extends boolean = false, Value = Variants[number]> = TypedColumnBuilder<{ kind: "ENUM"; variants: [...Variants] }, Optional, undefined, HasDefault, Value>;

FloatColumn

type

type FloatColumn<Optional extends boolean = false, HasDefault extends boolean = false, Value = number> = TypedColumnBuilder<"REAL", Optional, undefined, HasDefault, Value>;

InsertOf

type

type InsertOf<TTable> = TTable extends { readonly _initType: infer TInit } ? TInit : never;

InsertVerb

type

type InsertVerb<T, Init> = (values: Init) => WriteHandle<T>;

A callable verb declared over an insert operation.

IntColumn

type

type IntColumn<Optional extends boolean = false, HasDefault extends boolean = false, Value = number> = TypedColumnBuilder<"INTEGER", Optional, undefined, HasDefault, Value>;

JsonColumn

type

type JsonColumn<Output = JsonValue, Optional extends boolean = false, HasDefault extends boolean = false, Value = Output> = TypedColumnBuilder<JsonSqlType<Output>, Optional, undefined, HasDefault, Value>;

MarkConfig

type

type MarkConfig<Init> = {
synced?: Partial<Init>;
rejected?: Partial<Init>;
};

Per-fate row patches for mark.

MutationOp

type

type MutationOp<T, Init, Kind extends MutationOpKind = MutationOpKind> = {
readonly kind: Kind;
readonly table: TableProxy<T, Init>;
};

A table operation a verb is declared over; see insert, update, remove.

MutationOpKind

type

type MutationOpKind = "insert" | "update" | "remove";

Which operation a MutationOp performs.

MutationOptions

type

type MutationOptions<Row> = {
effects?: readonly EffectUnit<Row>[];
onSynced?: EffectHandlers<Row>["onSynced"];
onRejected?: EffectHandlers<Row>["onRejected"];
expiresAfterMs?: number;
};

Options accepted by mutation: declared units plus inline sugar.

MutationVerb

type

type MutationVerb<Op> = Op extends MutationOp<infer T, infer Init, "insert"> ? InsertVerb<T, Init> : Op extends MutationOp<infer _T, infer Init, "update"> ? UpdateVerb<Init> : Op extends MutationOp<infer _T, infer _Init, "remove"> ? RemoveVerb : never;

The callable shape of a declared verb, derived from its operation.

NestedApp

type

type NestedApp<TDef extends NestedSchemaDefinition> = { [mapped type] };

The value returned by defineNestedApp: one typed app per namespace, each exposing that namespace's tables under their unprefixed local names.

NestedAppRoot

type

type NestedAppRoot = {
readonly nestedAppRootBrand: true;
};

The opaque marker of a nested-app root. Only defineNestedApp produces values of this type, so APIs that require a root — store provisioning and the nested-table introspection helpers — state that precondition in their signatures instead of failing at runtime. The marker is type-level only; no property is added to the returned object.

NestedSchemaDefinition

type

type NestedSchemaDefinition = Record<string, SchemaDefinition>;

A nested schema definition: namespaces mapping to per-namespace tables.

NoticeConfig

type

type NoticeConfig<Row> = {
synced?: NoticeResolver<Row>;
rejected?: NoticeResolver<Row>;
ttlMs?: number | null;
};

Per-fate notice configuration for notice.

NoticeInput

type

type NoticeInput = {
message: string;
tone: "info" | "success" | "warning" | "error";
ttlMs: number | null;
};

One durable notice a notice unit enqueues. The queue is persistent and UI-agnostic: entries may be created at a boot re-arm with nothing mounted, and a component renders them later. tone classifies the message for the render; ttlMs bounds its life when the author sets no explicit dismissal.

NoticeResolver

type

type NoticeResolver<Row> = string | ((row: EffectRow<Row>) => string);

A notice message resolved from the settled row, or a static string.

PlainColumn

type

type PlainColumn<T> = T & { readonly plainColumnBrand: true };

A column excluded from sealing by the author; see plain.

PolicyContext

type

type PolicyContext<TApp extends AppLike> = {
policy: { [mapped type] } & { exists(relation: PermissionRelation): ExistsRelationCondition; union(relations: readonly PermissionRelation[]): PermissionRelation };
anyOf: (conditions: readonly unknown[]) => Condition;
allOf: (conditions: readonly unknown[]) => Condition;
isCreator: Condition;
allowedTo: AllowedToContext;
session: SessionContext;
};

PrivateTableColumns

type

type PrivateTableColumns<TCols> = { [mapped type] };

The column mapping applied by privateTable: plain-marked columns, reference columns, and byte columns keep their declared type; every other column becomes an EncryptedColumn of its view type.

RefColumn

type

type RefColumn<TargetTable extends string, Optional extends boolean = false, HasDefault extends boolean = false, Value = string> = TypedColumnBuilder<"UUID", Optional, TargetTable, HasDefault, Value>;

RemoveVerb

type

type RemoveVerb = (id: string) => WriteHandle<void>;

A callable verb declared over a remove operation.

Schema

interface

interface Schema<TSchema extends SchemaDefinition = SchemaDefinition> {
readonly definedSchemaBrand: CompactSchema<TSchema>;
}

SchemaDefinition

type

type SchemaDefinition = Record<string, TableSource>;

SchemaDsl

type

type SchemaDsl = Pick<typeof schema, "string" | "boolean" | "int" | "float" | "timestamp" | "bytes" | "json" | "enum" | "ref" | "array" | "add" | "drop" | "renameFrom" | "table" | "defineSchema" | "defineApp" | "defineSliceableApp" | "defineMigration" | "renameTableFrom" | "definePermissions" | "permissionIntrospectionColumns"> & { defineNestedApp: typeof defineNestedApp; defineNestedPermissions: typeof defineNestedPermissions; mergeNestedPermissions: typeof mergeNestedPermissions; flattenNestedSchema: typeof flattenNestedSchema; encryptedText: typeof encryptedText; encryptedJson: typeof encryptedJson; encryptedNumber: typeof encryptedNumber; encryptedDate: typeof encryptedDate; privateTable: typeof privateTable; plain: typeof plain; sharedEncryptedText: typeof sharedEncryptedText; sharedEncryptedJson: typeof sharedEncryptedJson; mutation: typeof mutation; effect: typeof effect; log: typeof log; trace: typeof trace; debug: typeof debug; notice: typeof notice; mark: typeof mark; chain: typeof chain; webhook: typeof webhook; insert: typeof insert; update: typeof update; remove: typeof remove };

The curated schema DSL type. Every Jazz member is the pinned Jazz 2 original, unchanged; deprecated members (rename) are omitted, and everything else re-exports one-to-one. The nested-namespace members (defineNestedApp, defineNestedPermissions, mergeNestedPermissions, flattenNestedSchema) are lofi-owned: a naming layer over the pinned defineSliceableApp, not part of the Jazz DSL.

SharedColumnConfig

type

type SharedColumnConfig = {
label: string;
kind: "text" | "json";
group: string;
groupIdColumn: string;
keys: string;
directory: string;
};

The wiring one shared column declares: where its group and keys live.

SharedColumnOptions

type

type SharedColumnOptions = Omit<SharedColumnConfig, "label" | "kind">;

Options wiring a shared column to its group, keys, and directory tables.

SharedFieldErrorCode

type

type SharedFieldErrorCode =
| "identity-missing"
| "key-pending"
| "unscoped-write"
| "corrupt"
| "peer-key-changed"
| "wrap-invalid"
| "no-directory-entry";

Stable categories for shared-field failures. identity-missing — a shared column was touched before the runtime installed the account's x25519 identity. key-pending — no field key is installed for the value's scope and generation yet; a normal state for a freshly added member. unscoped-write — a write reached the column transform without the mutation layer sealing it first. corrupt — a sealed value failed authentication. peer-key-changed — a peer's published public key no longer matches its pinned fingerprint. wrap-invalid — a wrapped key failed authentication or shape checks. no-directory-entry — a wrap was requested for an account that has not published a public key.

SharedFieldValue

type

type SharedFieldValue<T> = { state: "ready"; value: T } | { state: "pending-key"; scope: string; generation: number } | { state: "corrupt"; code: "corrupt" | "unscoped-write" };

The read state of a shared encrypted column.

StringColumn

type

type StringColumn<Optional extends boolean = false, HasDefault extends boolean = false, Value = string> = TypedColumnBuilder<"TEXT", Optional, undefined, HasDefault, Value>;

TableDefinition

type

type TableDefinition = Record<string, AnyTypedColumnBuilder>;

TableProxy

interface

interface TableProxy<T, Init> {
readonly _table: string;
readonly _schema: WasmSchema;
readonly _columnTransforms?: ColumnTransformMap;
readonly _rowType: T;
readonly _initType: Init;
}

Interface for table proxies used with mutations. Generated table constants implement this interface.

TimestampColumn

type

type TimestampColumn<Optional extends boolean = false, HasDefault extends boolean = false, Value = Date> = TypedColumnBuilder<"TIMESTAMP", Optional, undefined, HasDefault, Value>;

UpdateVerb

type

type UpdateVerb<Init> = (id: string, patch: Partial<Init>) => WriteHandle<void>;

A callable verb declared over an update operation.

WebhookOptions

type

type WebhookOptions = {
on?: ReadonlyArray<"synced" | "rejected">;
expiresAfterMs?: number | null;
maxAttempts?: number;
headers?: Record<string, string>;
};

Options for the webhook unit.

WhereOf

type

type WhereOf<TQuery> = TQuery extends { where(input: infer TWhere): unknown } ? TWhere : never;

WriteRejection

type

type WriteRejection = {
cause: "denied" | "expired";
code: string | null;
reason: string;
};

Why a write settled as rejected: the structured cause, code, and reason.

WriteStage

type

type WriteStage =
| "saving"
| "saved"
| "syncing"
| "synced"
| "rejected";

The closed, framework-owned write lifecycle. Stages are monotonic: saving → saved → syncing → synced | rejected. syncing is reserved for a runtime that can observe the transport; the current storage engine exposes no such signal, so today writes move from saved directly to synced or rejected and no handle ever reports syncing. Branch on saved vs settled, not on seeing syncing.