- Implemented a service worker (sw.js) that handles installation, activation, and fetch events. - Added caching logic for public assets and a fallback for offline navigation. - Created unit tests (sw.test.ts) to validate service worker behavior, including asset caching, navigation handling, and activation logic. - Ensured that private API routes and third-party requests are excluded from caching.
5.6 KiB
Progressive web app
Mina Habits has an installable production build with a public offline app shell. Its manifest supplies a stable app identity and root scope, standalone display, 192px and 512px icons, a separate maskable icon, app colors and descriptions. The HTML includes an Apple touch icon, mobile metadata, theme color and safe-area support. Browser chrome follows the selected light or dark theme.
Install from Install app in the footer. Supporting browsers show their native install prompt. Other browsers receive instructions for Add to Home Screen or Add to Dock. Installed windows hide the install action.
Offline behavior
After one successful online visit and service-worker installation, the application shell, fonts, icons and lazy browser chunks are available offline. Opening or reloading the app offline shows a reconnect screen instead of the browser error page. Nested routes also resolve correctly. Accounts retry on reconnection and periodically while a failed connection is visible, including when the browser incorrectly reports that it is online. An account request has a 10-second timeout; page navigations fall back to the cached shell after four seconds.
Account access, habit changes, sign-in, exports and Discord actions require a connection. Existing dashboard content can remain visible in an open window, but no account responses are persisted for offline use and no mutations are queued or replayed. Failed saves must be retried by the user. This is an offline fallback, not offline habit editing or background synchronization.
All /api traffic, including OAuth callback navigations, bypasses the service
worker. API responses have Cache-Control: no-store. Only explicitly public
build assets enter Cache Storage; server JavaScript, source maps, account data,
exports and third-party responses are excluded. Clearing site data removes the
public offline files; it does not remove account records from the server.
Build and updates
bun run build runs scripts/build-pwa.ts after Bun emits the browser and server
bundles. It derives a release identifier from the worker source and public asset
contents, copies a version-specific public HTML shell, and emits dist/pwa/sw.js
with an explicit precache list. A failed precache download prevents installation
of that release. The stable /sw.js route is served as JavaScript with
Cache-Control: no-cache and root service-worker scope.
Updates wait until existing windows close or the user chooses Update app, then Reload and update. The confirmation tells the user to save unfinished changes. Only the accepting window reloads; other windows retain their drafts and receive their own update action. Old static caches remain available for their lazy chunks. A subsequent activation with no open app windows removes obsolete Mina Habits caches, leaving unrelated caches untouched. Checking for updates on return to the app, reconnection and hourly supports long-lived installed windows.
The source manifest is src/pwa/manifest.webmanifest. Bun fingerprints its HTML
link; an explicit /manifest.webmanifest endpoint serves the same document. The
manifest's stable id keeps app identity independent of its asset filename.
Install icons reuse docs/design/mina-habits-icon.png; regenerate their exact
sizes with bun scripts/pwa-icons.ts. The maskable derivative adds opaque padding
to keep the monogram inside the central safe zone. Existing favicon and Apple
icons are preserved.
Serving requirements
Use the normal production build/start or Docker workflow at the root of an HTTPS
origin. Local testing may use http://localhost or http://127.0.0.1. The current
production configuration already requires an HTTPS origin outside loopback and
uses Caddy for TLS. No new deployment environment variables are required.
Service-worker registration is deliberately disabled in development/HMR mode;
run the production smoke check to verify caching. Deploy the entire build
atomically, including dist/pwa, rather than uploading individual browser files.
Do not apply immutable HTTP caching to /sw.js or add a proxy cache for /api.
PWA installation support and its interface vary by browser. Push notifications, background sync and app-store packaging are optional capabilities, not installation requirements. This implementation follows the current MDN installability requirements and the documented service-worker lifecycle.
Verification
bun run typecheck
bun test
bun x playwright install chromium
bun run test:pwa
bun run test:smoke
test:pwa copies the production build into a temporary directory, starts it with
a disposable SQLite database and no Discord credentials, then runs Chromium.
It verifies manifest parsing and Chrome installability errors, icon dimensions,
MIME/cache headers, first installation, offline startup and nested/lazy routes,
reconnection, responsive layouts, theme color, failed-update recovery, a two-window
update with an unsaved draft, API-cache exclusion, sign-out and mobile install
instructions. Screenshots are written to .cache/pwa/. Temporary data and the
server are removed when the check finishes. Worker unit tests cover routing,
installation failure and cache cleanup.
Before a public release, also exercise installation and standalone relaunch on a physical iPhone/iPad and Android device over the production HTTPS origin. Desktop Chromium checks and mobile emulation do not establish native OS installation or Safari behavior. The implementation does not claim a universal PWA certification.