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.