Troubleshooting
Start with:
deno task doctor
The report names the blocked capability or configuration and gives a remediation without printing environment values.
Doctor or build reports a PWA source blocker
Start with the first named file and action. The checks validate public/manifest.webmanifest as
JSON; require stable identity, names, same-origin launch scope, display mode, colors, and complete
icon roles; and inspect the dimensions and MIME types of local icons, shortcuts, and optional
screenshots. Asset filenames and branding are replaceable, and unknown optional manifest members
remain allowed.
If source checks pass but build reports dist/, do not hand-edit the output. Fix the author-owned
manifest, shell link, route, or asset, then rerun deno task build. The production check
deliberately treats HTML manifest links, the worker revision and scope, lofi-build.json, and
lofi-precache.json as one artifact so a partial or stale build cannot pass.
The app says configuration is incomplete
Managed sync requires both public values:
JAZZ_APP_ID
JAZZ_SERVER_URL
Set both, or remove both to run local-only. Do not move JAZZ_ADMIN_SECRET or BACKEND_SECRET into
client-visible variables.
If provisioning should replace an existing .env, review and back up the intended configuration,
then use the command's explicit --force option:
deno task jazz:provision --force
Durable storage is blocked
lofi requires a secure context, OPFS, SharedWorker, Web Locks, and MessageChannel. It stops instead of silently switching to memory-only data.
- Use a supported browser: Android Chrome 148+ or iOS Safari 16.4+.
- Use
localhostduring development or HTTPS on another device. - Avoid embedded/private browser surfaces that disable required storage APIs.
- Treat clearing site data as destructive unless the account has synced and the recovery phrase is safe.
Another tab is running an incompatible app version
Lofi stops persistent runtime startup when another tab for the same app is using an incompatible
browser broker configuration. This is reported as broker-incompatible in runtime diagnostics in
both local and managed mode.
- Close every other tab or installed-app window for this app.
- Return to the tab showing the recovery notice.
- Select Reload app once.
The package does not take over the existing broker, fall back to memory, or automatically enter a reload loop. Do not clear site data for this condition: closing the incompatible tabs and explicitly reloading creates the clean document boundary the persistent driver requires.
A task disappeared after changing app configuration
Check whether databaseName or the managed JAZZ_APP_ID changed. Both participate in the durable
storage namespace. Restoring the old values reopens the old namespace; copying rows between
namespaces is not automatic.
Preview says the build is missing or stale
Run:
deno task build
deno task preview
Do not hand-create dist/lofi-build.json; it must describe the output produced by the build.
The production build reports a secret leak
Remove the named server-only value from source, public files, logs, fixtures, and generated client configuration. Rotate a real credential if it entered source control or an artifact, then rebuild.
Browser tests skip or cannot launch
Set the base URL when running the opt-in example:
LOFI_E2E_BASE_URL=http://localhost:4321/ \
deno test -A tests/convergence_e2e_test.ts
The base URL must say localhost, not 127.0.0.1: the convergence gate enrolls a passkey to elect
sync, and an IP address is not a valid WebAuthn RP ID, so the ceremony never starts.
Install the pinned Chromium runtime if needed:
Local writes work but another device does not update
Check these states separately:
- The deployment has a complete public Jazz pair.
- The user explicitly elected to sync in
AccountGate. - The latest write reached global durability rather than only local durability.
- Both devices restored the same account identity.
- The deployed schema and permissions allow the operation.
“Sync configured” is not proof that a specific user opted in or that a specific write reached the server.
A device with an enrolled ticket came back local-only
The declared sync location persists as a sealed record whose key lives in the browser's IndexedDB. Clearing site data partially — IndexedDB without localStorage, or a browser "free up space" eviction — leaves the record in place but unopenable, and the device deliberately falls back to local-only rather than guessing. Local data is untouched. Re-enroll the ticket (paste it again) to declare the sink and resume syncing under the same account.
Sync is connected but writes to a self-hosted store never complete
Against a store with no deployed schema, writes hang rather than fail. When the active sink is a
ticket-gated node URL, the runtime checks the store at boot and records the result as storeStatus
in runtime diagnostics; the development inspector's Store row and the DeviceStatus panel
display it. no_schema means the store has never been provisioned for this app — follow
Store provisioning, then reload. The runtime only reports store state; it
never provisions or repairs a store on its own, and for non-ticket sinks the row stays
not checked.
Ticket enrollment runs the same check up front and refuses to keep an enrollment whose store answers
no_schema or rejects the ticket (SyncEnrollmentError), so this boot-time symptom is reached
through a store that regressed after enrollment, not through a fresh enrollment.
Enrolling a ticket fails with "no schema deployed"
The node accepted the ticket, but its store has never been provisioned for this app, so nothing could sync; enrollment was rolled back and the device is unchanged. Provision the store — see Store provisioning — then enroll again. A sync-only ticket cannot provision; the store's owner deploys the schema from a provision-scoped capability first.
Sync says it belongs to another account
Sync on a device is pinned to the account that elected it. When the account in hand differs — a
restored identity over a device that was already syncing, or a reset browser store that minted a
fresh local account — the runtime boots local-only with transport suppressed, the session reports
syncOwnerMismatch, and the DeviceStatus panel names the owning account when it is known. Nothing
is merged and nothing is deleted. Either stop syncing (which releases the pin, so the current
account may elect afterwards) or restore the owning account, then enable sync again.
The app stopped reloading automatically
Framework-driven reloads (sync election, account replacement) are budgeted per tab: a sequence of
reloads that never reaches a working runtime stops instead of cycling, and the failure surfaces as a
reload-loop runtime startup failure in the recovery UI and the device report. A reload cycle means
persisted state contradicts itself, often after a partially cleared browser store. Check the device
report's account and sync state, resolve what it names, then reload manually.
The installed app opened empty after Add to Home Screen
WebKit gives an installed web app its own storage container: cookies are copied at install time, but
OPFS, IndexedDB, and localStorage are not. Data created in a Safari tab before installing stays in
Safari's container — it is not lost, and the installed app cannot see it. When the runtime detects
this fork it renders a notice (.lofi-storage-fork-banner), and the DeviceStatus panel's
Storage container row reads fresh install — previous data is in the browser.
To move the data:
- Open the site in Safari (the browser, not the installed icon). The original data is there.
- In the account panel, turn on sync, or back up the account (reveal the recovery phrase or create a passkey backup) where a sync location exists.
- Return to the installed app and restore the account there; enrolled sync replays the data.
- Once the data appears, dismiss the notice.
Dismissing without restoring starts the installed app fresh; the Safari copy remains until Safari's site data for the origin is cleared. If no notice appeared (the flag cookie expires after seven days without a browser visit), the same steps apply. Background on the container fork and its limits is in Deployment.
Recovery does not restore a recent item
The phrase restores account authority, not unsynced device storage. Data returns only if it reached managed sync before the original device was lost or cleared.
Passkey restore was cancelled or cannot find an account
- Cancelled: retry the user-verification prompt, or continue with the recovery phrase.
- No recoverable passkey: confirm the passkey exists in the active provider. A legacy phrase-reveal guard is not an account backup.
- Different app hostname / RP-ID: open the canonical production hostname used during backup.
- Verification failed: unlock the platform/password-manager authenticator and retry.
- Unsupported browser/provider: use the recovery phrase. Provider availability is not portable across every iOS, Android, browser, and password-manager combination.
The package maps these states without printing credential IDs, account secrets, or phrases.
Sharing says managed sync is required
Private resources work local-only. Direct shares and groups require both a configured Jazz server
and the current account's explicit sync election. Back up and enable sync in AccountGate, then
retry. A configured deployment alone does not opt the current account into sync.