clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/prf-provision-custody
lofi / docs / auth-identity.md
auth-identity.md 7.0K rendered
1# Local-first accounts: open now, back up later23This document explains the account and custody model. For project setup and a shipping checklist,4see [Sync and recovery](sync-and-recovery.md).56lofi's identity model is **local-first**. A new app opens immediately on a private, on-device7account — no sign-in, no ceremony, nothing sent anywhere. From there the user can _elect_ to back up8and sync that account, and recover it on another device. Electing to sync preserves everything9created while local-only, because the account identity never changes.1011This is the pit of success: the app works the instant it loads, and the path to a durable,12multi-device account is one opt-in click — not a wall the user hits before they can do anything.1314## The three states15161. **Local-only (first boot).** The runtime opens a random per-device account17 (`BrowserAuthSecretStore.getOrCreateSecret`) and never waits. Writes are durable on the device;18 nothing reaches the network. This is the whole experience for a purely local app.192. **Backed up & syncing (elected).** When a managed Jazz app is configured, the user elects to back20 up and sync. Replication turns on, the existing local data pushes up under the _same_ identity,21 and a recovery phrase becomes available.223. **Recovered (in a fresh browser).** A recoverable passkey or the recovery phrase reconstructs the23 exact account secret. The old runtime is shut down, the new principal is opened with sync, and24 data that reached the sync provider flows back down.2526The **AccountGate** island (`src/islands/AccountGate.tsx`) drives states 2 and 3. It renders only27when a Jazz app is configured, because a recovery phrase only matters once there is somewhere to28sync to.2930## How the account survives the upgrade3132A lofi account _is_ a 32-byte secret. The Jazz account id is a function of that secret, so the same33secret always opens the same account — whether or not a sync server is attached:3435```36local-only: createDb({ appId, secret }) → account X, on this device37synced: createDb({ appId, secret, serverUrl }) → account X, now replicated38```3940Electing to sync recreates the runtime with `serverUrl` added. Because the secret (and thus the41account id) is unchanged, every write made while local-only still belongs to account X — no new42account, and nothing for the user to re-enter. Internally, the election is one durable record: lofi43opens the account's managed namespace, copies the rows written while local-only into it (waiting44only for local durability, so the copy also completes offline), and replicates them up through45normal sync once connected.4647## The recovery phrase is the portable backup4849The same 32-byte secret encodes as a 24-word recovery phrase (`RecoveryPhrase.fromSecret` /50`.toSecret`, via `jazz-tools/passphrase`):5152```53recovery phrase = words( account secret ) # the phrase *is* the account54```5556- **Back up** → create a recoverable passkey and reveal the phrase once. Both represent the exact57 same account secret.58- **Recover with a passkey** → the browser/provider releases the resident credential's secret after59 user verification → the same account opens.60- **Recover with the phrase** → type the words on a compatible device → the same account opens. The61 phrase is the portable bearer-secret fallback and does not depend on a passkey provider.6263Restoring replaces whatever account the device currently holds, so the UI confirms before discarding64a local-only account's un-backed-up data.6566### Two passkey credentials with different jobs6768`createRecoverablePasskeyBackup` uses Jazz's passkey-backup implementation to place the 32-byte69account secret in a resident, user-verifying platform credential. `restoreFromPasskey` can recover70the account from that credential. Availability follows the configured RP-ID and the browser,71operating system, and passkey provider; lofi does not promise universal cross-provider or72cross-platform portability.7374The older `createBackupPasskey` API is different: it enrolls a credential that only confirms a later75`confirmPhraseAccess` ceremony on the same browser. It does not contain a recoverable account76backup. The reference UI labels it a **local phrase-reveal guard** so the two credentials cannot be77confused.7879### Boot flow8081- **First boot** → local-only account opens immediately. No gate.82- **Back up & sync** → create a recoverable passkey when supported → reveal the recovery phrase to83 save → the user confirms the phrase is saved → sync turns on and the runtime recreates (a page84 reload), and the account replicates. Sync is never enabled while the phrase is still unread.85- **Return boots** → the same cached secret opens the same account, synced or not.86- **A fresh browser** → explicitly confirm replacement → restore from passkey or phrase → the old87 subscriptions, stores, workers, cached clients, and Jazz client shut down → the restored account88 starts and synced rows can arrive.89- **Stop syncing** → detaches the network and returns to local-only; the account and data are90 untouched, and electing again resumes against the same account.9192The framework side is exported by `@nzip/lofi` (`readAccountSession`,93`createRecoverablePasskeyBackup`, `restoreFromPasskey`, `revealRecoveryPhrase`, `enableSyncBackup`,94`stopSyncBackup`, `restoreFromRecoveryPhrase`). The gate is the author-owned example you can restyle95or replace.9697## Custody and recovery — stated plainly9899- **lofi does not run a recovery service.** The user holds the phrase; a recoverable passkey is held100 by the browser/platform provider. The Jazz sync service cannot reconstruct the account secret.101- **Provider sync has boundaries.** A passkey may sync inside one provider ecosystem, but RP-ID,102 account, browser, operating-system, and provider rules decide availability. Keep the phrase.103- **Lose every secret-bearing device, recoverable passkey, and phrase → lose the account.** Synced104 ciphertext alone does not restore identity authority.105106The exact pinned dependency audit and trust assumptions are retained in107[the passkey decision record](decisions/passkey-backup-alpha53.md).108109## Provisioning sync110111A managed Jazz app supplies `JAZZ_APP_ID` / `JAZZ_SERVER_URL` (the public pair the runtime reads)112plus server-only secrets. Generate one and write `.env` in one step:113114```sh115deno task jazz:provision # existing project116deno run -A jsr:@nzip/lofi/create --sync my-app # at scaffold time117```118119The generated app is unclaimed — claim it from the Jazz dashboard within the grace window shown, or120it expires. `.env` holds the server-only secrets and is git-ignored.121122## Optional: protecting the secret at rest123124The package auth runtime is a standalone device-credential primitive: enroll a WebAuthn passkey and125derive a credential-bound key (PRF) to encrypt data — including the account secret — **at rest**. It126is feature-detected and never faked. It is _not_ the account identity (deriving the account from a127credential was removed, since a derived key is a different account and cannot carry local-only data128forward). See [Advanced device auth](auth.md).