1# Tickets explained23<!-- Source: FelineStateMachine/lofi-node docs/app-ticket.md (the normative contract) and4 src/ticket.ts; lofi package/runtime/data-sink.ts for the app side. -->56lofi-node uses two kinds of ticket, and confusing them is the most common early stumble. This guide7is the conceptual tour; the [app-ticket contract](/node/docs/app-ticket) is the normative spec the8lofi side implements against.910## Two ticket kinds1112- A **node-pairing ticket** (`endpoint…`) is an iroh endpoint address. It names a _node_ so another13 node can dial it. It is used by `lofi-node pair` and never by browsers or apps.14- An **app-connect ticket** (`lofisync1.…`) is the credential _you_ hand to a lofi app so it syncs15 against your node. It carries the store's app id and the gate URL with a 256-bit secret embedded16 in its path.1718## Why the secret lives in the URL path1920Jazz clients preserve a base path in their server URL and reject query parameters. By making the21ticket's URL `http(s)://host:port/t/<secret>`, every request the client ever makes — sync WebSocket22connects, catalogue reads, admin calls — carries the secret with **zero client changes**. The gate23validates the secret (timing-safe, digest against digest), strips the prefix, and proxies to the24loopback-only Jazz server. The lofi app uses the ticket URL verbatim as its sync server; that is the25whole integration.2627## Scopes2829- **`sync`** (the default, and what an absent scope means — every pre-scope ticket keeps meaning30 transport-only): sync, catalogue reads that transport needs, and the metadata-only `store-status`31 preflight. Admin routes answer the same `401 {"error":"invalid_ticket"}` as an unknown secret, so32 a probe learns nothing.33- **`provision`**: a strict superset — everything above plus store administration. The gate injects34 the node's own `X-Jazz-Admin-Secret` for provisioning requests and strips any inbound one, so the35 admin secret never transits the client. Possession of a provision ticket **is** the36 store-administration opt-in; issue one per provisioning context, not per device.3738A ticket with an unrecognized scope is rejected outright by the lofi parser — never silently granted39less than it claims.4041## Revocation4243- The node stores only SHA-256 digests; the ticket string is displayable once, at issuance.44- Unknown and revoked secrets are indistinguishable to probers: both get45 `401 {"error":"invalid_ticket"}`, on HTTP and on the WebSocket upgrade.46- Revoking a ticket mid-session closes its live WebSockets with close code **4001** within a couple47 of seconds. The app treats the stored sink as dead and surfaces re-enrollment — it never silently48 retries forever.4950## The security posture, honestly5152An app ticket is a **bearer credential** with 256-bit entropy. Anyone holding it can sync as an53authorized transport peer; identity and row-level permissions remain Jazz's local-first layer on54top, enforced by the deployed policy, not by the ticket. Plain http is acceptable on a trusted LAN;55beyond one, front the gate with TLS — installed PWAs generally require a secure origin anyway. Issue56tickets per device or context so revocation is scoped.5758## What the app does with it5960You paste; the app does the rest. The ticket is validated (against the same rules this node61enforces; the two repos share machine-readable conformance fixtures), declared as the device's sync62location, and sync is elected in one step. The app keeps the ticket URL in a sealed device-local63record (encrypted at rest under a device-bound key) and never surfaces it through its session64snapshot — only the host and your label show. A provision ticket gets split first: the app asks the65node for a derived sync ticket to store, and the provision original is either sealed behind your66passkey or kept only in memory with your password manager holding the durable copy. The derived67ticket is linked to its parent — revoking the provision ticket revokes it too. For app developers,68the framework call behind that paste is `enrollSyncTicket`, documented in69[Sync and recovery](/docs/sync-and-recovery).