clone

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

docs/ticket-custody-accuracy
lofi-node / docs / hosting-lofi-apps.md
hosting-lofi-apps.md 5.9K rendered
1# Hosting a lofi app on your own sync node23lofi-node is the first-class way to self-host the sync backend for a4[lofi](https://github.com/FelineStateMachine/lofi) app: the browser keeps speaking Jazz's protocol5(`adapter: "jazz"` untouched); only the server URL changes. This walkthrough was validated end to6end against lofi's reference app (`apps/reference`) with real Chromium clients.78## 1. Host a node910Install once (`deno install -g -A -n lofi-node jsr:@nzip/lofi-node/cli`), run ad hoc with11`dx -A jsr:@nzip/lofi-node/cli …`, or download a compiled binary from the GitHub releases. Then:1213```sh14lofi-node init --dir ./data --port 4802 --public-url http://192.168.1.10:480215lofi-node start --dir ./data16```1718New inits are **ticket-gated**: only requests carrying an issued app-ticket secret reach Jazz (the19embedded Jazz server binds loopback-only; lofi-node's access gate owns the public port). `--open`20opts out for dev setups. `data/config.json` holds the app id and the admin secret you'll need below.2122### Storage choice2324`--storage-path /mnt/nas/lofi` puts the SQLite store on any mounted location (NAS, synced volume);25`--memory` keeps everything ephemeral. The path is probed writable at boot — an unwritable location26fails fast by name. jazz-napi supports exactly SQLite-directory and memory today; for cloud off-site27durability, replicate the SQLite file (Litestream-style to S3, or a snapshotting volume). The28`storage.type` discriminator in config is the seam future providers slot into.2930### Issue an app ticket3132```sh33lofi-node ticket issue --dir ./data --label phone34# → lofisync1.eyJ2IjoxLCJhcHBJZCI6… (shown once; secret is never stored)35lofi-node ticket list --dir ./data36lofi-node ticket revoke <id> --dir ./data # live sockets close with 400137```3839The user pastes the ticket into their lofi app (tell them to let their password manager save it —40the secret is shown once and the node keeps only a hash). The app never stores a ticket in41cleartext: a sync ticket persists as a sealed record under a device-bound key and its URL becomes42the sync server; a provision ticket is split first through the scope-down exchange — the app stores43a derived sync ticket and keeps the provision original passkey-sealed or memory-only. Format and44app-side flow: [app-ticket.md](app-ticket.md).4546## 2. Get the app's schema into the store4748A store with no deployed schema is unusable — client writes hang (preflight with49`GET <ticket.url>/store-status`, see [app-ticket.md](app-ticket.md)). Two paths publish a schema:5051**Primary — runtime provisioning via a provision ticket** (the lofi#109 opt-in flow): issue52`lofi-node ticket issue --provision --label app-setup` and hand it to the app. On enrollment the app53splits it through the scope-down exchange (everyday transport rides a derived sync ticket; admin54capability unlocks through a passkey ceremony when provisioning actually runs). The app (or any55jazz-tools `deploy` caller) uses the ticket URL as `serverUrl` with a placeholder admin secret — the56gate strips inbound admin headers and injects the node's own for provision-scoped requests, so the57real secret never leaves the node. Slice-by-slice merge deploys (`createTables` migrations, union58permissions) flow through the same ticket; old-hash clients keep syncing across a merge.5960**Fallback — operator-side deploy from the app directory**, using the same schema-project deploy the61lofi dev flow uses (also easiest via a provision ticket URL as `serverUrl`; the admin secret62argument can then be a placeholder):6364```ts65// deploy-schema.ts (run: deno run -A deploy-schema.ts)66import { deploy } from "./node_modules/.deno/jazz-tools@2.0.0-alpha.53/node_modules/jazz-tools/dist/dev/catalogue-project.js";67await deploy({68 serverUrl: "http://127.0.0.1:4802",69 appId: "<app id from config.json>",70 adminSecret: "<admin secret from config.json>",71 schemaDir: "./src",72});73```7475> The schemaDir-project deploy is not yet a public jazz-tools export — a `lofi-node deploy` CLI verb76> wrapping this is a natural follow-up once jazz-tools exposes it (or lofi's provision command grows77> a self-host mode).7879## 3. Build the app against the node8081```sh82JAZZ_APP_ID=<app id> JAZZ_SERVER_URL=<ticket url> deno task build83deno task preview84```8586`JAZZ_SERVER_URL` must be http(s) — lofi's preflight rejects ws URLs; the browser client derives the87WebSocket endpoint itself (`…/apps/<uuid>/ws`, preserving the ticket's `/t/<secret>` base path). On88an open-mode node use the node URL directly. Beyond a trusted LAN, front the gate with TLS;89installed PWAs require a secure origin. (Once lofi ships its runtime serverUrl override, the90build-time env becomes unnecessary — the app enrolls the ticket at runtime instead; see91[app-ticket.md](app-ticket.md).)9293## Validation status (2026-07-18, jazz-tools 2.0.0-alpha.53)9495- Reference app **builds, boots, and serves** against a lofi-node backend; two real Chromium clients96 (lofi's `createTwoClientFixture`) load the app, hydrate the store, go offline, and edit97 independently.98- lofi's opt-in browser convergence gate (`tests/convergence_e2e_test.ts`) currently fails at the99 convergence stage with `CatalogueWriteDenied` from the server when the second (cloned identity)100 client reconnects — **identically against lofi-node and against lofi's own managed dev server**101 (controlled experiment, same error, same catalogue object id). lofi-node has exact behavior parity102 with the first-party local server; the denial is a jazz-tools alpha.53 local-server behavior,103 upstream of this project. Headless two-client convergence (jazz-tools `createDb` clients, same104 account, concurrent offline edits) passes against lofi-node — see `tests/convergence_test.ts`.105106## Pairing two homes107108On the second node: `lofi-node pair <ticket from the first>` (or `SyncNode.pair(ticket)` at109runtime). Catalogue reads/writes and sync then flow to the peer over iroh — deploying the schema to110either node lands on the root through the tunnel's HTTP proxy.