Self-host your first sync node
By the end of this tutorial you will have a sync node running on your own machine, an app-connect ticket in hand, and a lofi app you use syncing against it. You are the app's user here, not its developer: the app stays exactly as it was deployed, and pointing it at your node is entirely your call. Nothing below requires a cloud account, a static IP, or TLS on a trusted LAN — one binary and one paste.
1. Install lofi-node
Install the tool from JSR, which puts lofi-node on your PATH:
deno install -g -A -n lofi-node jsr:@nzip/lofi-node/cli
For a single self-contained binary instead, compile from a checkout of lofi-node:
deno task compile # → dist/lofi-node
Either way, full support covers macOS arm64 and Linux x86_64 (including the container image); on Windows and arm64 Linux the node is limited (why).
2. Initialize and start
lofi-node init --dir ./data --port 4802 --public-url http://192.168.1.10:4802
lofi-node start --dir ./data
init writes data/config.json: a generated Jazz app id, the node's secrets, your access mode, and
storage choice. New inits are ticket-gated — only requests carrying an issued app-ticket secret
reach the store; --open opts out for dev setups. The embedded Jazz server binds loopback-only; the
node's access gate owns the public port, so the gate is both enforcement and reachability.
--public-url is the base address that will be embedded in the tickets you issue — use the address
other devices on your network can reach. --storage-path /mnt/nas/lofi puts the store on any
mounted location; --memory keeps everything ephemeral (storage choices).
start prints the gate URL and, when the mesh is up, the node-pairing ticket for
pairing a second node.
3. Issue an app ticket
lofi-node ticket issue --dir ./data --label phone
# → lofisync1.eyJ2IjoxLCJhcHBJZCI6… (shown once; the secret is never stored)
The ticket is one string carrying the store's app id and the gate URL with an access secret embedded
in its path. It is displayed exactly once — the node keeps only a digest. Issue one per device or
context (--label is for your own bookkeeping), so revocation stays scoped:
lofi-node ticket list --dir ./data
lofi-node ticket revoke <id> --dir ./data # live connections close within seconds
Scopes matter later: a plain ticket is transport-only; ticket issue --provision mints one that can
also set up the store's schema. What the string actually contains and why
the secret lives in the URL path: Tickets explained.
4. Enroll the ticket in a lofi app
Paste the ticket into the app — a lofi app with sync exposes enrollment in its account UI. That declares your node as the device's sync location and elects sync in one step; the data you already made in the app pushes up under the same account identity. Nothing about the app changes or redeploys — pointing it at your node is your call, not the app developer's.
(If you are the app developer: the framework side is one call, enrollSyncTicket, documented in
Sync and recovery.)
5. Verify
GET <gate-url>/healthanswers without a ticket — safe for liveness probes.GET <ticket-url>/store-statusanswers for any valid ticket with metadata about the store's schema. A fresh node reports{ "schema": { "deployed": false } }— that is expected, and it is exactly the state the store provisioning tutorial resolves. An app should not sync against a store with no schema; its writes would hang rather than fail.lofi-node status --dir ./datashows configuration, tickets, upstream, and mesh state without starting anything.
Where you are
You have a node, a ticket, and an enrolled app — but the store may not have a schema yet. Continue with Provision a store, or connect a second node with Pair two homes.