Before we go into this post I earlier planned for a different topic however in the course of my vibe experience something else came up.
There is a question nobody asks when they set up a service worker:
Who owns the application lifecycle?
With a conventional PWA, the browser primarily orchestrates installation, caching, and the service worker lifecycle. The application participates in those decisions, but the browser ultimately controls when service workers are activated, how storage is managed, and what "installed" means on a given platform.
Spirit gives a different answer. The application owns installation, storage, and updates. The browser simply provides the runtime.
That is not a small distinction. It starts with one reframe:
spirit-sw.js is firmware, not middleware.
Every conventional PWA uses the service worker as a proxy — it intercepts requests and decides whether to serve from cache or go to the network. Spirit uses the service worker differently. It never serves your application files. It never touches Cache Storage. It serves a single hardcoded bootstrap string that knows how to resurrect the application from IndexedDB. The service worker is a bootloader. It boots the same way every time.
Everything else follows from that.
What Spirit is
Spirit is an IDB-native installation and boot system. Four moving parts:
spirit-grave.js — the storage layer. Pure IndexedDB, no DOM, no network. Buries a file as a Blob, exhumes it later. Nothing else.
spirit-reg.js — the installer. Runs once, on the first real online visit. Reads spirit-manifest.json, fetches every listed file, buries each one in IndexedDB under gnoke:spirit/files.
spirit-sw.js — the bootloader. Intercepts every navigation request and responds immediately with a hardcoded HTML string baked into the worker itself. That string contains an inline IDB reader — no separate fetch. It reconstructs the application from the grave: CSS injected as <style>, images converted to blob URLs, JS executed as classic scripts in manifest order. Zero network requests on boot.
spirit-revive.js — the updater. Exposes Spirit.reviveFromNetwork(). Call it from a button inside the running application. It re-fetches the manifest, re-buries changed files, and reloads only if something actually changed. Nothing runs in the background automatically.
The lifecycle comparison
With a conventional PWA:
Browser requests URL
→ SW intercepts, checks cache
→ serves cached or fetches fresh
→ background SW update check runs independently
→ new SW waits for all tabs to close (or skipWaiting forces it)
→ user may receive updated code mid-session without requesting it
With Spirit:
Browser requests URL
→ SW responds immediately with hardcoded bootstrap
→ bootstrap opens IndexedDB, reconstructs app from buried files
→ app runs — no network involved
→ update only happens when Spirit.reviveFromNetwork() is called
→ app controls when to prompt, when to reload
The update is part of the application's lifecycle, not the browser's.
What this makes possible
Given a valid installation, startup is deterministic. The same boot sequence runs every time — online or offline, first launch or hundredth. There is no cache miss, no conditional fetch, no race between a new service worker and open tabs.
Spirit is designed so updates replace the installed application as a single managed operation, avoiding the mixed-version states that can occur when independently cached assets drift. One call, one operation, one reload decision.
The bootloader is stable. spirit-sw.js is the only file that triggers the browser's own SW update cycle — and it is designed to change rarely, like actual firmware. The application underneath it can update as often as it needs to without touching the bootloader.
Where this matters and where it does not
Spirit is not a general improvement over Cache Storage. For a news site, a marketing page, a content application — the browser's built-in model is exactly right. HTTP caching, ETags, Cache Storage, and the SW update cycle were designed for that world and they work well in it.
Spirit makes sense for a different category: software that happens to be delivered via the web.
- Offline-first tools where network access is intermittent or absent
- Browser OS environments where the app manages its own disk
- Industrial or embedded browser runtimes where update timing must be controlled
- Editors and development tools that need to own their own boot sequence
Spirit was built for GnokeStation — a browser-native OS where IndexedDB is the disk, a SharedWorker is the kernel, and tabs are processes. In that context, the conventional PWA model has a fundamental mismatch. Spirit fits the architecture instead of fighting it.
The honest trade-offs
What you give up:
- Browser DevTools understand Cache Storage natively. An IDB-backed virtual filesystem is more opaque to debug.
- HTTP-level caching is bypassed entirely. Spirit's current change detection compares blob sizes — a content hash would be more robust and is worth adding.
-
reviveFromNetwork()buries files sequentially. A tab closed mid-update leaves a partially updated grave. A staged swap — write new files first, then commit — would make updates truly atomic. - The first visit still requires a real online session. Unavoidable.
-
navigator.storage.persist()lowers eviction risk but does not eliminate it.
What you gain:
- The web delivers the application once. After that it runs from local storage entirely.
- No dependency on Cache Storage eviction policies.
- The application decides when it updates and what the user sees when it does.
- The bootloader is stable across application versions.
The deeper idea
Spirit doesn't try to replace the PWA model. It explores a different one: treating the browser as a runtime capable of hosting installed software, rather than a document viewer that happens to cache files.
The web is used once to install the application. From then on, the browser hosts a locally managed runtime whose lifecycle is controlled by the application — not by HTTP caching semantics, not by the SW activation queue, not by storage eviction policy.
Whether that trade-off is right depends entirely on the kind of application you are building.
Spirit is built for the second kind.
And for the records my semantics for file names are my way of expressing myself, what matters to me is that the aim is achieved not the name of the file 🤓
Spirit is part of GnokeStation v2 — a browser-native OS built on SharedWorker as kernel, IndexedDB as disk, and tabs as processes. Built by edmundsparrow.

Top comments (6)
How does a Spirit app dig itself out if a bad update or a corrupt grave means it never boots? The SW answers every navigation from IndexedDB, and reviveFromNetwork() is a button inside the running app, so the recovery path seems to sit inside the exact thing that's broken. With a normal PWA I can hard refresh back to the network, but here that door looks closed unless the user knows how to clear IndexedDB by hand. Is there an escape hatch for that?
Fair point. Right now, if the installation becomes corrupted badly enough that it can't boot, there's no built-in recovery path—you'd need to reinstall. That's a limitation of the current implementation, not the architecture itself.
The obvious next step is a tiny recovery mechanism in the bootloader itself, like a hidden reset switch that can wipe the grave and trigger a fresh install without asking the user to clear browser storage manually. Even desktop software sometimes needs to be reinstalled.
The partial-update failure mode you flagged as a nice-to-have is actually your single biggest risk, and it's worse than "a tab closed mid-update leaves a partial grave." Because your SW serves a hardcoded bootstrap that unconditionally reconstructs from IndexedDB, a partial grave doesn't fail loud — it boots. You get an app running new CSS against old JS, or a JS file that references an asset that never got reburied, with zero network fallback to bail you out. That's the exact mixed-version state you're claiming Spirit avoids, except now it's baked into local disk and survives reloads until someone calls reviveFromNetwork() again.
The staged-swap you mention isn't optional polish here — it's the thing that makes the whole "single managed operation" claim true. And IndexedDB gives you the primitive for free: do the entire rebury inside one readwrite transaction (or write to
gnoke:spirit/stagedand flip a singleactivepointer record as the last write). IDB transactions abort atomically if the tab dies, so a killed update rolls back to the last good grave instead of leaving a Frankenstein one. Until that's in, "deterministic startup" only holds for installs that never got interrupted mid-update.One thing I'd want to see spelled out: what does the bootstrap do when it opens IDB and finds nothing — evicted grave, or a first navigation that races the installer? Since the SW never touches the network, the failure path there matters as much as the happy path, and it's the part the firmware analogy conveniently skips.
Thanks for the thoughtful critique. I think I may not have explained the architecture clearly enough.
Spirit wasn't conceived as a replacement for PWAs. It emerged from discovering that IndexedDB could reliably resurrect HTML, which led me to ask: if it can reconstruct HTML, why not the rest of the application?
The distinction I'm trying to make is that the PWA is the launch surface, while the Spirit installation is the application payload. The PWA remains responsible for browser integration—a stable launcher and install icon, with recovery intended to live at that layer. Spirit manages the installed application stored in IndexedDB.
That separation is intentional. It keeps the browser-facing layer small and stable while allowing the application itself to own its storage and lifecycle. Recovery, atomic updates, and rollback are still engineering work to be completed, but they build on that separation rather than changing it.
You can conceptually think of the PWA launcher as playing a BIOS-like role. It isn't the application itself; it's the stable entry point that decides whether to boot the Spirit installation, recover it, or reinstall it. The installed application lives in IndexedDB, while the launcher remains intentionally small and stable.
@edmundsparrow The BIOS analogy is where I want to push, because it's doing a lot of load-bearing work here. A real BIOS is stable precisely because it lives in different, more durable storage than the thing it boots — separate flash, its own write path, its own failure domain. Your launcher and your Spirit payload both live behind the same browser storage machinery and the same service worker lifecycle. When the browser evicts under storage pressure, or a user clears site data, or an SW update goes sideways, both layers are exposed to the same event. The separation is logical, not physical, and BIOS separation is physical. That's the gap I was poking at.
That said — I think you actually answered my real concern, which is that you know recovery, atomic updates, and rollback are the hard part and haven't hand-waved them as done. That's the honest version, and I'll take "still to be built, but built on this seam" over "already handled" every time. Where it lives or dies is exactly there: can the launcher detect a half-written or evicted payload and reinstall cleanly, without a partially-applied update bricking the boot? That's the load-bearing engineering, not the IndexedDB-resurrects-HTML discovery that got you here.
One concrete thing I'd want to see before I bought the analogy: what happens to an in-flight update when the tab closes mid-write? If the answer is "the launcher notices an incomplete generation on next boot and rolls back to the last good one," you've got your BIOS story. If it's "undefined," that's the first bug report waiting to happen.
You're right that logical separation isn't physical separation, and I won't stretch that comparison further than it holds.
On the concrete question: an in-flight update never touches the currently-active generation. New files are staged under a new generation number alongside the old one; only the last step — one transaction — flips active_gen and status to the new generation together. If the tab dies before that, active_gen never moved, so the bootloader just keeps serving the last good generation on the next boot. No undefined state, no partial boot — worst case is an update that silently didn't take, not a bricked one.
That's implemented now, not aspirational — happy to be poked at it again once it's public.