Skip to content

Progressive Web App (PWA)

Patrimoine is an installable PWA with offline support for cached static assets. The service worker is built by vite-plugin-pwa (workbox generateSW strategy, configured in frontend/vite.config.ts’s VitePWA() plugin call), with security-first caching to prevent cross-user data leakage on shared devices.

For the user-facing side (installing, offline use, updates) see Install as an app.


The web app manifest is generated by vite-plugin-pwa from the manifest option in vite.config.ts and emitted as manifest.json (manifestFilename: 'manifest.json') — there is no hand-written public/manifest.json.

Property Value
App name Patrimoine
Display Standalone
Icons favicon.svg, icon-192.png, icon-512.png (any); icon-maskable-192.png, icon-maskable-512.png (maskable)
Theme color #0e0f12 (same value as index.html’s <meta name="theme-color">)
Background color #0e0f12
Orientation Not set (any orientation) — deliberate: the layout is responsive, and standalone windows on desktop and tablet must be free to rotate or resize

The service worker is generated at build time by vite-plugin-pwa’s workbox generateSW strategy — there is no hand-written public/sw.js. It is registered from frontend/src/components/PwaUpdatePrompt.tsx via registerSW() from the virtual module virtual:pwa-register, called with { immediate: true } inside a useEffect.

Registration failures are handled by an onRegisterError callback that only logs to the console in development mode (import.meta.env.DEV); in production, failures are silently caught to avoid polluting logs.

vite.config.ts sets registerType: 'prompt' (not the default 'autoUpdate').

'prompt' instead calls an onNeedRefresh callback and never reloads on its own.

Resource Strategy Rationale
Static assets (precached via workbox.globPatterns: ['**/*.{js,css,html,svg,png,ico,woff2}'], minus globIgnores: assets/pdf/** and the en, es, de and it catalogues under assets/locales/) Cache-first (precache + cleanupOutdatedCaches()) Static assets are content-hashed; cached versions are safe to serve. French stays precached as the default language and the offline fallback
PDF export chunks (assets/pdf/) and the en, es, de and it catalogues (assets/locales/) Runtime cache-first in the lazy-chunks cache, 16 entries, only 200 responses Fetched and stored the first time they are used instead of at install; safe because the names are content-hashed; cleared by the logout purge like every other cache
Navigation requests (/, deep links) Served from the precached index.html via a workbox NavigationRoute, except paths under /api/, /auth/ and /health (see navigateFallbackDenylist in vite.config.ts) Keeps client-side routing working offline while never intercepting API/auth calls; the health probe must always answer from the backend
API calls (/api/*), auth calls (/auth/*) and the health check (/health) Excluded from the SW entirely (denylisted, network-only) Prevents cross-user data leakage on shared devices — API/auth responses are never cached or served from the SW

The /health entry is the anchored pattern /^\/health(?:[/?]|$)/: it matches /health, /health/ and /health?x, but not a future SPA route that merely starts with “health”.

There is no CLEAR_CACHES service-worker message. Cache purging on logout is done directly by the app in frontend/src/hooks/useLogout.ts: it calls caches.keys() and caches.delete() on every Cache Storage entry (when 'caches' in window) before POSTing to /auth/logout and redirecting to /. This ensures no sensitive data remains in Cache Storage on shared devices.

The generated service worker listens for a SKIP_WAITING message and calls self.skipWaiting() in response — but only in response. Nothing calls clientsClaim(). In practice this means:

  • The new service worker installs and waits, but does not take control of open pages on its own.
  • PwaUpdatePrompt.tsx is the only caller of updateSW() (the function registerSW() returns), and it only calls updateSW(true) when the user clicks “reload” on the update toast shown by its onNeedRefresh handler — see Update Flow below.
  • Because clientsClaim() is never called, a newly activated worker still doesn’t take over already-open tabs outside of that explicit user action; the user always sees a consistent version of the app rather than a half-updated state mid-session.
  1. registerSW({ immediate: true, onNeedRefresh, onRegisterError }) registers the SW on app load.
  2. When workbox detects a new SW version, onNeedRefresh fires. PwaUpdatePrompt.tsx shows a persistent sonner toast (duration: Infinity) with a “reload” action button.
  3. Only if the user clicks that button does the app call updateSW(true), which sends SKIP_WAITING to the waiting worker and reloads the page once it activates.

The theme initialization script is at public/theme-init.js. It is loaded as an external script (not inline) to comply with Content Security Policy (CSP).

Before React hydrates, the script reads the user’s skin and mode preferences from localStorage ('skin' and 'mode' keys) and sets two independent attributes on <html>:

  • data-skin (one of fintech, azur, noir, swiss, aurora, ledger)
  • data-mode (resolved to light or dark — system is resolved against window.matchMedia('(prefers-color-scheme: dark)') before it’s written, so data-mode never holds the literal string 'system').

This prevents a flash of the wrong theme (FOUC) on page load.

See Design System — Skins and Modes for the full mechanism (this script is a hand-kept mirror of useSkin.ts’s valid-value lists, kept in sync by a regression test).


index.html includes:

  • <meta name="color-scheme" content="light dark"> — lets the browser natively style form controls and scrollbars
  • A single <meta name="theme-color" content="#0e0f12">

The manifest uses the same value for theme_color and background_color, so the splash screen, the standalone title bar and the browser chrome agree. It is not synced to the active skin: a light skin keeps a dark title bar in the installed app.


File Description
public/favicon.svg Rising area chart design; also used as the any-purpose manifest icon
public/icon-192.png 192px PWA icon (any purpose)
public/icon-512.png 512px PWA icon (any purpose)
public/icon-maskable-192.png 192px maskable PWA icon: full-bleed #0e0f12, no transparency, glyph inside the safe zone
public/icon-maskable-512.png 512px maskable PWA icon: full-bleed #0e0f12, no transparency, glyph inside the safe zone
public/apple-touch-icon.png 180px opaque iOS Home Screen icon, linked from index.html; iOS prefers it over the manifest icons
public/icons.svg Icon sprite

The last three PNGs are rendered once from frontend/scripts/icon-maskable.svg by sh frontend/scripts/generate-icons.sh (needs rsvg-convert) and committed; the build does not generate them.


  • Cached static assets (HTML, CSS, JS, icons) are available offline via the workbox precache
  • The PDF export and a language other than French are available offline only after their first use online (they are stored in the lazy-chunks runtime cache, not in the precache)
  • API calls require a network connection — no offline data access (denylisted from the SW)
  • Previously viewed data may be available if the app shell was cached, but dynamic data is not persisted offline

There is no manually-maintained version string to bump.

vite-plugin-pwa’s workbox generateSW build generates a fresh precache manifest (with a content hash per asset, revision set accordingly) on every production build, and cleanupOutdatedCaches() removes stale precache entries automatically when a new SW activates — so cache invalidation is automatic per deploy rather than driven by a hand-bumped version.