--- name: compose-wasmjs-pwa description: >- Turn a Compose Multiplatform (Kotlin/Wasm) web app into an installable PWA with a deliberate software-update experience and a correctly separated data cache: Workbox precaching, a startup splash gate so the old version never flashes on launch, a user-chosen update notification (pulsating button, banner, silent auto-reload, or quiet indicator), an audit of the app's existing data caching against what a service worker changes, offline handling, and the server cache headers that make update detection actually work. Use this whenever the user wants to add PWA support, offline support, precaching, a service worker, a web app manifest, an "installable web app", or any kind of auto-update / update-prompt / "New Software" button to a Compose or Kotlin/Wasm (wasmJs) app — even if they only mention "workbox", "service worker", "update on reload", "cache my API responses", "make it work offline", or "why is my deploy not picked up". Also use it when a Compose wasmJs app caches the wrong things, serves stale data, or fails with `Cannot run program "npx"`, and when replacing an older hand-written service worker whose clients are stuck on a cached shell ("users are still on the old version", "the old sw.js won't go away", "how do I unregister the previous service worker"). Prefer this skill over hand-rolling service-worker or Cache API code for Compose/wasmJs targets. --- # Compose wasmJs → PWA with controlled updates and a clean data cache Converts a Compose Multiplatform app that already has a **wasmJs** browser target into a PWA whose update behavior is deliberate and whose caching is correctly partitioned. Bundles the exact files to copy and the wiring steps. > **Validated 2026-07-25** against Kotlin **2.4.10**, Compose Multiplatform > **1.11.1**, Gradle **9.5.1**, Ktor **3.5.1**, Workbox **7.4.0**, and the > ComposePWA plugin **0.7.0-alpha03** — by converting a real production app and > deploying it. > > Several claims here are pinned to those versions and should be re-checked > rather than trusted if you are far from them: the `JsException` / `kotlin.Error` > behaviour of a failed fetch (B2 row 5), the `cleanupOutdatedCaches` predicate > (B2 row 2 — read it out of the generated `workbox-*.js`), and anything about the > ComposePWA plugin, which is a 0.x alpha and may move under you. > > Licensed under Apache-2.0 (see `LICENSE`). Copyright 2026 mpMediaSoft GmbH. The work splits into two phases: - **Phase A — software.** Precaching, update detection, the startup gate, and the update notification. Always applies. Starts with A0: retiring any service worker a previous build already put in users' browsers — do that first, because clients stuck behind a cache-first old worker cannot be reached by anything you ship later. - **Phase B — data.** An audit of the app's existing data layer, offline handling, and the server cache headers. Applies only if the app fetches anything at runtime. Most existing data layers *store* things correctly; the defects are almost always in the failure path, which nobody exercises. Read `references/how-it-works.md` for the software-side rationale and tuning knobs, `references/data-and-deployment.md` for the caching model and deployment, and `references/notification-styles.md` when implementing the chosen notification. You do not need any of them to apply the skill — this file is enough for the common path. ## The idea that makes the rest follow > The service worker owns the software cache. The app owns the data cache. > Neither reaches into the other's territory. Software is what the build emits and is identical for every user until the next deploy. Data is what the app fetches at runtime. The browser will happily cache both through the same machinery, which is exactly the problem — a service worker that caches API responses serves them stale and hides deploys. Everything below is downstream of keeping those two apart. ## Step 0 — Ask the user before writing any UI Notification style is a product decision, not a default. An app that can lose unsaved work must never silently reload; a kiosk dashboard should never nag. Ask, then implement one scheme — do not build several and leave dead code behind. Ask these, together, before touching the project: 1. **Update notification** — which of these when a new version appears *while the app is running*: - **A. Pulsating "New Software" button + Cancel/Update dialog** — the validated default. Best when the user may have unsaved state. - **B. Silent auto-reload** — no UI; the tab reloads itself. Only for apps that genuinely hold no user state. Say so plainly when offering it: this is the one option that can destroy work, and it does so invisibly. - **C. Banner / snackbar with a Reload action** — calmer than A, still actionable. - **D. Quiet indicator** — a dot or label; the user reloads when they like. Mention the trade-off: nothing draws the eye, so a long-lived tab can run an old version for days. 2. **Does the app fetch data at runtime?** (API calls, documents, polled content.) If yes, Phase B applies. If no, skip it entirely. 3. **If Phase B applies — data-status indicator**: pulsing dot / static dot / text only when offline / none. If the user has no opinion, recommend **A** plus a **pulsing dot**: the combination this skill was validated with. Record what they picked; it decides which asset variants you copy in A6 and B3. ## Before you start: understand the project shape Compose Multiplatform apps are usually split into two kinds of module. Identify them: - The **wasmJs application module** — has `wasmJs { browser(); binaries.executable() }` and a `main()` calling `ComposeViewport { ... }`. The PWA plugin, web resources, and the `main()` change go here. (Often keeps its sources under `commonMain` even though it only targets wasmJs.) - The **shared UI module** — where the root `@Composable` (often `App`) lives, usually in `commonMain` with several targets. The `PwaUpdate` bridge and the update UI go here, as does the app's existing data layer if it has one. Both may be the same module — fine, everything lands there. Confirm: which module is the executable, which file has `main()`, which composable `ComposeViewport` mounts, its package, and every target the shared UI compiles for (each needs an `actual` for `PwaUpdate`). If anything is ambiguous — module names, package, whether the UI is multiplatform or wasmJs-only — ask rather than guess. ## Placeholders in the bundled assets - `{{PACKAGE}}` — package of the shared UI / root composable. - `{{APP_NAME}}` / `{{APP_SHORT_NAME}}` — display names for manifest and title. - `{{WASM_JS_FILE}}` — the JS entry file the wasm build emits, referenced by `index.html`: `.js` (e.g. `webApp.js`). Match any existing `