Skip to main content

lofi

local-first meta-frameworkv0.9.1 · alpha · MIT

Installable web apps where the network is optional and the user holds the keys.

deno run -A jsr:@nzip/lofi/create my-app

Requires Deno 2.9+. Astro + Preact + Jazz 2, generated.

A phone containing a growing plant. Its roots run below the soil line inside the device; a dotted line arcs to a second, smaller phone.

Open now. Back up later.

First launch

A private account, on the device

Every user gets an on-device account at first open, even when the app has no sync service configured at all. Nothing to sign up or waiting.

Every launch after

Reads from local storage, not the network

The UI reads durable local data instead of waiting on a round trip. When storage fails, lofi surfaces it rather than quietly falling back to memory and risking the data.

If the user wants it

The same account, made portable

Connect managed Jazz sync and a user can back up the account they already have — recovering it with a passkey where supported, or a 24-word phrase. Data made offline comes along, and a fresh device can connect and recover in either order.

deno task jazz:provision
or scaffold with --sync from the start.

at the core of the idea

Pull the cable.

This is the starter task app the generator gives you. Turn the network off, keep typing on both devices, then turn it back on. Edits from both sides survive and every replica converges on the same result: same-field collisions resolve last-writer-wins, everything else merges cleanly. That is what the CRDT buys you. A reworked build of this starter runs installable at demo.lofi.host, deployed from the latest release.

Synced

This phone

  • Water the ferns
  • Repot the monstera
  • Order more perlite

Your other device

  • Water the ferns
  • Repot the monstera
  • Order more perlite

A simulation, drawn to the same model (not a live Jazz session). For the real one, and for the part where you close the laptop and it is all still there tomorrow: deno task dev.

The parts you would otherwise write yourself.

Local-first data

The UI reads from durable local storage instead of waiting on the network. Storage failures are surfaced, never silently swapped for memory.

Identity from first launch

Each user begins with a private on-device account, even when the app has no sync service configured.

Optional sync and recovery

Users make that same account portable when you connect managed Jazz sync: a passkey where available, a recovery phrase as the portable fallback. Or they sync to a node they run — the app needs zero changes either way.

Server-blind fields

Columns are sealed on the client before they sync — s.privateTable encrypts by default, and shared sealed columns extend that to groups. The keys stay with users; the server relays what it cannot read.

Verbs with effects

Tables are the nouns; mutations declared with s.mutation are the verbs. Their sync-boundary effects run from a durable journal on the device that performed the intent — once, even across restarts.

Narrow collaboration templates

Private resources, direct shares, and fixed-role groups sit behind a small @nzip/lofi/access API. Raw Jazz permissions stay available as an escape hatch.

A shell that defends itself

Generated apps ship a strict Content-Security-Policy, detect a forked storage container before it splits the account, and show honest progress on the one first load that needs the network.

Local-first test helpers

Playwright-backed fixtures cover offline writes, multiple clients, convergence, and readiness — without hand-timed sleeps.

Explicit mobile support

Unsupported browsers get a clear explanation, not a half-working app that risks the user's data.

You own the product.
lofi owns the plumbing.

my-app/
├── deno.json                    # tasks + one pinned version
├── public/                      # manifest, product icons
├── src/
│   ├── app.ts                   # storage, sync, passkey config
│   ├── schema.ts                # persisted data model
│   ├── permissions.ts           # private, shared, or group
│   ├── islands/                 # interactive Preact UI
│   ├── layouts/                 # document shell
│   ├── pages/                   # Astro routes
│   └── styles/                  # product styling
├── tests/                       # local-first journeys
├── .lofi/                       # generated tooling (ignored)
└── dist/                        # production PWA (ignored)

@nzip/lofi                       # storage, identity, sync, PWA

A generated project keeps your source separate from the versioned framework package. Product work stays in the files above; framework behaviour is imported, never copied into your tree. Upgrading the package updates the runtime without a migration through your own source.

  • Yours. Schema, permissions, config, routes, islands, styles, tests.
  • lofi's. Storage, identity, sync, lifecycle, and PWA plumbing — imported from one pinned version.
  • Generated. Regenerated on demand and git-ignored. Never edit by hand.

See the exact generated-project map →

stack and version policy

Pinned on purpose.

The data layer is an alpha, so every upgrade is reviewed and validated before it reaches a generated project.

LayerChoicePinned at
Data / syncJazz 2, CRDTs, OPFS2.0.0-alpha.53
UI runtimePreact islands, thin adapterPreact 10 / Astro 7
ShellPrerendered Astro, static hostAstro 7
ToolchainDeno tasks, npm compatDeno 2.9

What it isn't, yet.

Built for mobile web apps where offline is a requirement, not a best-effort enhancement. Before you commit:

  • It is an early alpha release. Expect the surface to move, and do not yet trust it with data you cannot afford to lose.
  • The data layer is Jazz 2 alpha, deliberately pinned to a reviewed version. Known engine defects at the current pin are documented in decision records and pinned by automated canaries that fail loudly when a version bump changes the behavior.
  • Sealing has limits. Private and shared columns are encrypted on the client before they sync, but structure, timing, and presence stay visible, and sealing protects future content, not merged history. The threat model states exactly what the server can and cannot see.
  • Browser floors are real. Android Chrome 148+ and iOS Safari 16.4+.
  • Recovery is user-controlled. lofi keeps no recoverable account material on the server, so the phrase has to be kept safe.

Every project, a unified toolset.

Every generated project exposes these as deno task <name>.

Everyday development
devRuns the Astro dev server and prints runtime state.
doctorChecks readiness without printing config or secrets.
testRuns the deterministic local-first suite.
buildCreates a static production build in dist/.
previewServes the production build locally.
Sync, schema, deployment
jazz:provisionCreates a managed Jazz app and configures .env.
schema:validate
schema:deploy
Validates and publishes the Jazz schema.
migrations:create
migrations:push
Authors and pushes schema migrations.
deploy
deploy:create
Hosts the static build on Deno Deploy.