Store provisioning
import * as mod from "jsr:@nzip/lofi/schema/store";
Opt-in store provisioning for nested apps: classify a sync store's deployed schema against this app's slice, and create or update only this app's namespaces in it.
A user-supplied store is shared infrastructure — another app's tables may already live in it. Provisioning therefore works as a merge over the stored schema: the head schema is fetched verbatim, this app's tables are appended or verified, every other table (and its policies) is preserved byte-for-byte, and the change deploys as an ordinary migration advancing the store's one permissions head. The safety invariant is enforced here, before anything is published: an app may only create tables under its own declared namespaces, and any generated change naming a table outside them is a hard error.
Every operation needs store administration — supplying the admin secret,
or holding a provision-scoped app-connect ticket whose gate injects it, is
the user's opt-in. Nothing in the normal sync path calls this module; the
one exception is readTicketStoreStatus, the metadata-only preflight
any valid ticket may call.
provisionStore
function
async function provisionStore(options: ProvisionStoreOptions): Promise<StoreProvisionResult>
Creates or updates this app's slice in the store. The stored head schema is
fetched verbatim and only extended: missing tables of this app's namespaces
are appended (key order of everything else preserved — the schema hash is
serialization-sensitive), sibling tables and their policies carry through
untouched, and the change publishes as schema + added-table migration +
a permissions bundle chained to the current head. The app's own compiled
schema is additionally registered and connected to the head, so this app's
clients — which declare that schema on the wire — are never disconnected.
Drift is never repaired: a store whose copy of this app's namespaces
differs from the declaration throws with the differing tables.
readStoreStatus
function
async function readStoreStatus(app: NestedAppRoot, target: StoreTarget): Promise<StoreStatus>
Classifies the store's deployed schema against this app's slice without
changing anything. app must be a nested-app root from
s.defineNestedApp — the namespace is what scopes an app's view of a
shared store. no_schema matters beyond provisioning: against an empty
store, client writes hang rather than fail, so callers should reach this
state before ever attaching sync to a fresh store.
readTicketStoreStatus
function
async function readTicketStoreStatus(ticketUrl: string): Promise<TicketStoreStatus>
The metadata-only store preflight any valid app-connect ticket may call —
no admin capability required. A self-hosted node answers
GET <ticket.url>/store-status from its own loopback store, which is what
lets a sync-only client learn no_schema (where writes hang) before ever
attaching sync, and prompt toward the provisioning opt-in instead.
StoreProvisionError
class
class StoreProvisionError extends Error {
constructor(code: StoreProvisionError["code"], message: string);
readonly name: string;
readonly code: "http" | "schema-drift" | "not-nested" | "outside-namespace" | "policy-literal";
}
Raised when provisioning is refused or the store rejects a request.
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.
ProvisionStoreOptions
type
type ProvisionStoreOptions = {
app: NestedAppRoot;
permissions: CompiledPermissions;
target: StoreTarget;
};
What provisionStore deploys, and where.
StoreProvisionResult
type
type StoreProvisionResult = {
status: "created" | "updated" | "unchanged";
headHash: string;
};
The outcome of a provisioning run.
StoreStatus
type
type StoreStatus =
| { state: "no_schema" }
| { state: "ok"; headHash: string }
| { state: "schema_out_of_date"; headHash: string; missingTables: readonly string[] }
| { state: "schema_drift"; headHash: string; driftedTables: readonly string[] };
How a store's deployed schema relates to this app's slice. State literals are snake_case because they relay the sync node's status vocabulary verbatim.
StoreTarget
type
type StoreTarget = {
serverUrl: string;
appId: string;
adminSecret?: string;
};
The store to provision: where it is, which app it hosts, and the admin opt-in.
TicketStoreStatus
type
type TicketStoreStatus =
| { state: "deployed"; appId: string; headHash: string }
| { state: "no_schema"; appId: string }
| { state: "store_unavailable" }
| { state: "ticket_rejected" }
| { state: "unsupported" };
The result of the ticket-scoped store-status preflight. State literals are snake_case because they relay the sync node's status vocabulary verbatim.