Skip to main content

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.