Skip to content

feat(pwa): make the web build installable, with an in-app update prompt - #1206

Open
njbrake wants to merge 1 commit into
thunderbird:mainfrom
njbrake:pr8-installable-web-app
Open

feat(pwa): make the web build installable, with an in-app update prompt#1206
njbrake wants to merge 1 commit into
thunderbird:mainfrom
njbrake:pr8-installable-web-app

Conversation

@njbrake

@njbrake njbrake commented Aug 6, 2026

Copy link
Copy Markdown

Note: this PR description was drafted by Claude via back-and-forth with @njbrake. The reasoning and decisions are his; the prose is Claude's.

Adds a web app manifest, maskable icons, a generated service worker, and a prompt to hand over to a new build. This is the only route onto a phone home screen that does not go through an app store, and the web build had no update path at all: a stale client's only recourse was clearing site data, which does not reliably work because OPFS survives it.

Three constraints shaped the worker config:

  • Precache the app shell only (about 2MB: index.html, entry chunk, main stylesheet). A **/* glob precaches 263 entries and 31MB, because dist carries roughly 17MB of wasm (each blob emitted twice) plus multi-MB lazy chunks. The tradeoff is explicit: this is not a work-offline app.
  • Do not intercept the API. /v1/ and /.well-known/ are on the navigation fallback denylist and there is no runtimeCaching, since a stale API answer is worse than an offline error. SSE streams reach the network untouched.
  • Cross-origin isolation must survive. The app needs crossOriginIsolated for OPFS. Cached navigation responses retain nginx's COOP/COEP headers; verified still true after a reload served by the worker.

Handover is explicit. registerType: 'prompt' means a new build installs and waits, because swapping chunks under a live React tree breaks the session. Reload posts SKIP_WAITING then reloads once the worker reports activated; with no clientsClaim, waiting on controllerchange would hang.

The prompt renders at the app root so it survives the states where the app cannot render, including the init-error screen. Disabled when Tauri drives the build.

_Note: this PR description was drafted by Claude via back-and-forth with @njbrake. The reasoning and decisions are his; the prose is Claude's._

Adds a web app manifest, maskable icons, a generated service worker, and a prompt to hand over to a new build. This is the only route onto a phone home screen that does not go through an app store, and the web build had no update path at all: a stale client's only recourse was clearing site data, which does not reliably work because OPFS survives it.

Three constraints shaped the worker config:

- **Precache the app shell only** (about 2MB: index.html, entry chunk, main stylesheet). A `**/*` glob precaches 263 entries and 31MB, because `dist` carries roughly 17MB of wasm (each blob emitted twice) plus multi-MB lazy chunks. The tradeoff is explicit: this is not a work-offline app.
- **Do not intercept the API.** `/v1/` and `/.well-known/` are on the navigation fallback denylist and there is no `runtimeCaching`, since a stale API answer is worse than an offline error. SSE streams reach the network untouched.
- **Cross-origin isolation must survive.** The app needs `crossOriginIsolated` for OPFS. Cached navigation responses retain nginx's COOP/COEP headers; verified still true after a reload served by the worker.

Handover is explicit. `registerType: 'prompt'` means a new build installs and waits, because swapping chunks under a live React tree breaks the session. Reload posts `SKIP_WAITING` then reloads once the worker reports `activated`; with no `clientsClaim`, waiting on `controllerchange` would hang.

The prompt renders at the app root so it survives the states where the app cannot render, including the init-error screen. Disabled when Tauri drives the build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant