clone

a self-hosted sync node for lofi apps: jazz sync over iroh, ticket-gated, with no static ip or cloud needed

release/0.3.0
lofi-node / docs / site / sliceable-apps-and-shared-stores.md
sliceable-apps-and-shared-stores.md 4.6K rendered
1# Sliceable apps and shared stores23<!-- Source: lofi #109 (design + conformance), lofi docs/store-provisioning.md and4 docs/examples/nested-namespaces.md, lofi-node #2; jazz-tools typed-app surface. -->56Your store is shared infrastructure. You bring the store; the apps you use are tenants in it. This7guide is the mental model that makes that safe: what a slice is, how one store's schema grows app by8app, and why an app cannot damage a neighbor even though the store trusts its administrator.910## A slice is a naming discipline, not a partition1112Jazz's pinned DSL compiles **one schema per store** — one hash, one migration lineage, one query13planner. `defineSliceableApp` derives typed sub-app views over that single schema, and lofi's14`s.defineNestedApp` builds the naming layer on top: an app declares namespaces, its tables flatten15to global names with a reserved separator (`taskapp` + `tasks` → `taskapp__tasks`), and app code16uses unprefixed typed handles as if the namespace were its whole world.1718Nothing at the storage layer partitions those tables — every table is a sibling of every other in19the one store. The **slice** is the set of tables under an app's declared namespaces, and the20namespace prefix is what makes ownership checkable.2122## The store's schema grows slice by slice2324The store enforces exactly one schema at a time — the head of a migration lineage. When a second app25joins an occupied store, its provisioning _merges_: the stored schema is fetched verbatim, the app's26missing tables are appended, and the union deploys as an ordinary migration advancing the head.27Devices running the previous schema keep working, because their schema stays connected to the head28through the migration chain.2930Four states describe any store relative to any app — the same taxonomy the tooling reports:3132| State | Meaning |33| -------------------- | ------------------------------------------------------------------- |34| `ok` | The enforced schema carries this app's slice exactly. |35| `no_schema` | Nothing is deployed. Writes would hang; never sync in this state. |36| `schema_out_of_date` | The store lacks some of this app's tables. Remedy: opt-in update. |37| `schema_drift` | The store's copy of the app's own namespaces differs unexplainably. |3839`no_schema` deserves its emphasis: against an empty store the engine's writes hang rather than fail.40That is why the node exposes a metadata-only `store-status` preflight any valid ticket can call, and41why lofi classifies before attaching sync instead of writing and hoping. `schema_drift` is the42opposite discipline — it is surfaced with the differing tables and **never** auto-repaired; a store43whose copy of your namespace changed under you needs a human, not a merge.4445## The honesty invariant4647An app's provisioning may create tables **only under its own declared namespaces**. Everything else48in the store — sibling apps' tables _and their access policies_ — carries through a merge49byte-for-byte. Any generated change naming a table outside the app's namespaces is a hard error50before a single request is made. A flat (non-nested) app declares no namespace and therefore gets no51tenant rights: it may only provision a store it wholly owns.5253Two properties fall out, both verified against a real server in lofi's conformance suite:5455- **Continuity.** The first app's devices write and read across a second app's merge untouched.56- **Policy preservation.** A user who couldn't read a gated row of the first app before the merge57 still can't after it — the joining app re-publishes the union permissions with the sibling's58 policies recovered from the store, unchanged.5960Stated honestly: this is a _framework_ boundary, not cryptography. Store administration (the admin61secret, or a provision-scoped ticket) can technically do anything; the invariant keeps apps built on62lofi honest through the surface they actually use. The node-side complement is that a provision63ticket never exposes the admin secret itself — the gate injects it — so holding provisioning power64never means holding a credential you could leak.6566## Where each piece lives6768- Declaring namespaces and per-namespace permissions:69 [nested app namespaces](/docs/examples/nested-namespaces) in the framework docs.70- The classifier and merge flow an app runs: [store provisioning](/docs/store-provisioning).71- Doing it against a node with a provision ticket: [Provision a store](provision-a-store.md).72- The ticket semantics underneath: [Tickets explained](tickets-explained.md) and the73 [app-ticket contract](/node/docs/app-ticket).