---
source_hash: "096fe45f"
title: "PWA (Progressive Web App)"
weight: 140
---

# PWA (Progressive Web App)

[Vite-PWA Documentation](https://vite-pwa-org.netlify.app/)

A **Progressive Web App (PWA)** is an application that combines the best of web and mobile. It installs on the home screen, works offline, and offers a smooth experience close to a native app.

This page covers the complete chain: Service Worker, manifest, front/API origins, offline startup, update and installation.

> [!IMPORTANT]
> Two points are sources of recurring errors and are treated in detail below: the **navigation fallback** (without it, the application does not restart offline) and the **same-origin constraint** between front and API.

## Two Service Worker Modes

`vite-plugin-pwa` offers two strategies. SmartMaker uses both, depending on projects. You need to know which one is in place before touching the configuration, because **options are not interchangeable**.

| Mode | Who writes the Service Worker | Where cache is configured |
| --- | --- | --- |
| `generateSW` (default) | the plugin, entirely | `workbox` block in `vite.config.js` |
| `injectManifest` | you, in `mobile/src/sw.js` | in your `sw.js`, in Workbox code |

> [!WARNING]
> In `injectManifest` mode, the options `workbox.runtimeCaching`, `skipWaiting` and `clientsClaim` placed in `vite.config.js` are **completely ignored**. They only exist in `generateSW` mode. This is the most frequent cause of the symptom "my VitePWA configuration doesn't work".

Reference project status:

| Project | Mode | Manifest |
| --- | --- | --- |
| smartboot (skeleton) | `injectManifest` | dynamic (SmartAuth) |
| smartInterventions | `injectManifest` | static (Vite) |
| capfullpos | `generateSW` | dynamic (SmartAuth) |
| offlinepropale | `generateSW` | dynamic (SmartAuth) |

A project created with SmartBoot therefore starts in `injectManifest` mode.

### generateSW Mode

```javascript
// vite.config.js
VitePWA({
  registerType: 'autoUpdate',
  workbox: {
    globPatterns: ['**/*.{js,css,html,ico,png,svg,json}'],
    cleanupOutdatedCaches: true,
    maximumFileSizeToCacheInBytes: 3000000,
    skipWaiting: true,
    clientsClaim: true,
    runtimeCaching: [
      {
        urlPattern: /\/api\/(home|profile)/,
        handler: 'NetworkFirst',
        options: {
          cacheName: 'api-cache',
          expiration: { maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 * 7 },
          cacheableResponse: { statuses: [0, 200] },
          networkTimeoutSeconds: 10,
        },
      },
      {
        urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/,
        handler: 'CacheFirst',
        options: {
          cacheName: 'images-cache',
          expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 * 30 },
        },
      },
    ],
  },
  injectRegister: "auto",
  includeAssets: ["favicon.ico", "assets/*", "favicon.png", "apple-touch-icon.png"],
  manifest: false,
})
```

With `appType: 'spa'`, the plugin automatically adds a `navigateFallback` to `index.html`. Offline restart on a client route therefore works without writing anything else.

### injectManifest Mode

This is the SmartBoot skeleton mode. It is chosen because the Service Worker must carry SmartCommon's Web Push handlers, which `generateSW` does not allow.

```javascript
// vite.config.js
VitePWA({
  registerType: 'autoUpdate',
  strategies: 'injectManifest',
  srcDir: 'src',
  filename: 'sw.js',
  injectManifest: {
    globPatterns: ['**/*.{js,css,html,ico,png,svg,json}'],
    maximumFileSizeToCacheInBytes: 3000000,
  },
  // The SW is registered manually in src/main.jsx via virtual:pwa-register
  injectRegister: false,
  includeAssets: ["favicon.ico", "assets/*", "favicon.png", "apple-touch-icon.png"],
  manifest: false,
})
```

Key points:
- `globPatterns` goes in `injectManifest`, not in `workbox`
- `injectRegister: false` because registration is done manually (see below)
- all cache behavior now lives in `mobile/src/sw.js`

## The Service Worker in injectManifest Mode

The file `mobile/src/sw.js` **belongs to the project**. The plugin only injects the list of files to precache via `self.__WB_MANIFEST`.

### Complete Skeleton

```javascript
// mobile/src/sw.js
import {
    precacheAndRoute,
    cleanupOutdatedCaches,
    createHandlerBoundToURL,
} from "workbox-precaching";
import { registerRoute, NavigationRoute } from "workbox-routing";
import { NetworkFirst, CacheFirst } from "workbox-strategies";
import { ExpirationPlugin } from "workbox-expiration";
import { CacheableResponsePlugin } from "workbox-cacheable-response";
import { registerPushHandlers } from "@cap-rel/smartcommon/sw";

// 1. Precache build assets (mandatory in injectManifest)
precacheAndRoute(self.__WB_MANIFEST);

// 2. Equivalent of cleanupOutdatedCaches: true
cleanupOutdatedCaches();

// 3. SPA navigation fallback (see the box below)
registerRoute(new NavigationRoute(createHandlerBoundToURL("index.html"), {
    denylist: [/^\/api\//, /^\/api\.php\//, /\/[^/?]+\.[^/?]+$/],
}));

// 4. Runtime caching (equivalent of workbox.runtimeCaching)
registerRoute(
    /\/api\/(home|profile)/,
    new NetworkFirst({
        cacheName: "api-cache",
        networkTimeoutSeconds: 10,
        plugins: [
            new CacheableResponsePlugin({ statuses: [0, 200] }),
            new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 * 7 }),
        ],
    })
);

registerRoute(
    /\.(?:png|jpg|jpeg|svg|gif|webp)$/,
    new CacheFirst({
        cacheName: "images-cache",
        plugins: [new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 * 30 })],
    })
);

// 5. Equivalent of skipWaiting + clientsClaim
self.addEventListener("install", () => {
    self.skipWaiting();
});

self.addEventListener("activate", (event) => {
    event.waitUntil(self.clients.claim());
});

// 6. Web Push (shared handlers provided by smartcommon)
registerPushHandlers({
    defaultIcon: "/images/pwa-192x192.png",
    defaultBadge: "/images/pwa-64x64.png",
});
```

### Navigation Fallback, Essential for Offline

> [!WARNING]
> This is the number one trap of `injectManifest` mode. Unlike `generateSW`, **no `navigateFallback` is added implicitly**. The SmartBoot skeleton now provides this route; if you take over a project created before, or if you write your `sw.js` from scratch, verify that it is present.

Without this route, only `index.html` is precached, not the client paths. An offline reload on `/interventions` or `/produit/12` finds nothing in the precache, goes to the network and fails with `net::ERR_INTERNET_DISCONNECTED`. Observed symptom: **blank screen on offline restart**, even though the Service Worker is active and the cache is full.

```javascript
registerRoute(new NavigationRoute(createHandlerBoundToURL("index.html"), {
    // Never redirect API calls or file requests
    // (anything with an extension): only real application
    // navigations fall back to the shell.
    denylist: [/^\/api\//, /^\/api\.php\//, /\/[^/?]+\.[^/?]+$/],
}));
```

The `denylist` is essential: without it, the Service Worker would return `index.html` in response to API calls or file downloads.

> [!NOTE]
> The two API patterns are not redundant. A PWA deployed in `pwa/` is built with `VITE_API_URL=/api.php/`: its calls start with `/api.php/`, which the `/^\/api\//` pattern does not cover. Only the generic "anything with an extension" pattern caught them, which is fragile.

Reference implementation: `smartInterventions/mobile/src/sw.js` and the SmartBoot skeleton's `sw.js`.

### Service Worker Registration

In `injectManifest` mode with `injectRegister: false`, registration is explicit in `mobile/src/main.jsx`:

```javascript
import { registerSW } from "virtual:pwa-register";

registerSW({
  immediate: true,
});
```

> [!NOTE]
> Do not leave both `injectRegister: "auto"` and a manual call to `registerSW`: the Service Worker would be registered twice.

## Manifest

Two approaches coexist in SmartMaker. Choose before starting, they do not combine.

| Approach | Vite Configuration | Link in index.html | Customization |
| --- | --- | --- | --- |
| Dynamic Manifest SmartAuth | `manifest: false` | explicit, mandatory | Dolibarr constants, per entity |
| Static Manifest Vite | `manifest: { ... }` block | generated, to be removed | in code, at build time |

### Dynamic Manifest Served by SmartAuth

This is the choice of the SmartBoot skeleton and most modules. The manifest is produced by SmartAuth's `PwaController` from the Dolibarr module's constants, allowing each client to have their own name and icons without rebuilding the application.

In `mobile/index.html`:

```html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="manifest" href="api.php/manifest.webmanifest">
    <link rel="icon" type="image/png" href="api.php/icon/64">
    <link rel="apple-touch-icon" href="api.php/icon/192">
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>
```

In `vite.config.js`: `manifest: false`.

#### SmartAuth Routes

| Route | Description |
| --- | --- |
| GET `api.php/manifest.webmanifest` | dynamically generated manifest JSON |
| GET `api.php/icon/{size}` | PWA icon at requested size (64, 192 or 512) |

> [!WARNING]
> The URL is indeed `manifest.webmanifest`, including the extension. A link to `api.php/manifest` returns a routing error.

These two routes are not protected: the browser must be able to load them before any authentication.

#### Dolibarr Constants

The prefix is the module name in uppercase.

| Constant | Description | Default |
| --- | --- | --- |
| `{MODULE}_PWA_NAME` | full application name | company name, then module name |
| `{MODULE}_PWA_DESCRIPTION` | description | empty |
| `{MODULE}_PWA_BG_COLOR` | background color | `#ffffff` |
| `{MODULE}_PWA_THEME_COLOR` | theme color | `#000000` |

> [!NOTE]
> There is no constant for the short name: `short_name` is automatically derived from the first 12 characters of `{MODULE}_PWA_NAME`.

#### Non-Configurable Fields

The dynamic manifest fixes these values and does not expose them as constants:

```json
"id": "/",
"scope": "/",
"start_url": "/",
"display": "standalone",
"prefer_related_applications": false
```

> [!TIP]
> The question comes up often: `"display": "standalone"` is **already active**. An application installed from a dynamic manifest therefore opens in its own window, without address bar. There's nothing to add.

This choice is assumed and will not change: the dynamic manifest only serves what truly depends on the Dolibarr instance, i.e., name, description, colors and icons. `display`, `start_url`, `scope` and `orientation` are application properties, not of the client hosting it: they belong to the build. Exposing them as constants would mean entrusting Dolibarr parameterization with decisions that only the application author can make, and multiplying the combinations to test.

If you need to modify `display`, `start_url`, `orientation` or add `shortcuts`, switch to static manifest.

> [!WARNING]
> `id` and `scope` should not be modified lightly. `id` is the key by which the browser recognizes an already installed application. Changing it makes your application a **new** application in the browser's eyes: existing installations are no longer recognized, and the detection described below stops working for those who already had the app installed.

#### Self-Declaration for Installation Detection

The dynamic manifest declares itself:

```json
"related_applications": [
  { "platform": "webapp", "url": "https://your-app.example.fr/pwa/api.php/manifest.webmanifest" }
]
```

This is what makes `navigator.getInstalledRelatedApps()` usable: without this entry, the call returns an empty list on Android and the install banner is re-proposed to users who already have the application on their home screen.

The URL is **absolute** and built from the current request, never with `dol_buildpath()`: the application is served on its own virtualhost, which Dolibarr does not know. Since the `Host` header is provided by the client, it is validated; if it is unusable, `related_applications` is simply omitted rather than publishing a URL pointing to an origin chosen by a third party.

Nothing to do on your side: this is served automatically. If you switch to static manifest, however, this entry must be written by you.

#### Icons

The `PwaController` looks for the icon in this order:

- Custom icon sent from module administration, in `{dir_output}/pwa/icon_{size}.png`
- Icon shipped with the module, in `pwa/images/pwa-{size}x{size}.png`
- Default: blue square generated with module initials (requires GD)

Served sizes are 64, 192 and 512. Any other value defaults to 512.

### Static Manifest Generated by Vite

This is the smartInterventions choice. It is suitable when the manifest is the same for all clients and you want control over all fields.

```javascript
// vite.config.js
VitePWA({
  // ...
  manifest: {
    name: "SmartInterventions",
    short_name: "SmartInterventions",
    start_url: "/",
    display: "standalone",
    background_color: "#ffffff",
    theme_color: "#dc2626",
    icons: [
      { src: 'images/pwa-64x64.png', sizes: '64x64', type: 'image/png' },
      { src: 'images/pwa-192x192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
      { src: 'images/pwa-512x512.png', sizes: '512x512', type: 'image/png' },
    ],
  },
})
```

In this case, **remove the `<link rel="manifest">` from `index.html`**: Vite injects it itself. Two competing links produce an ignored or inconsistent manifest.

## Front Origin and API Origin

This is the most frequently blocking question in development.

### The Rule

A manifest must be served from **the same origin** as the HTML document that references it. A front on `http://localhost:5173` and an API on `https://dolibarr.local` are two distinct origins: the browser refuses the manifest and displays a message like *"The manifest must have the same origin as the page"*.

The same constraint applies to the Service Worker: it only controls its origin.

> [!IMPORTANT]
> It is therefore **not possible** to durably co-locate a front and an API on two different domains for a PWA. The solution is not to configure CORS, it is to **bring both to the same origin**.

### In Production: Same Origin by Construction

The PWA is served from the module's `pwa/` folder, on the same host as Dolibarr:

```
https://erp.client.fr/custom/mymodule/pwa/            <- the front
https://erp.client.fr/custom/mymodule/pwa/api.php/    <- the API
```

The build Makefile also forces the API URL to be relative just before building:

```bash
echo "VITE_API_URL=/api.php/" >| ./mobile/.env
```

then restores the original value of `.env` so as not to break the development environment. The production PWA therefore has no absolute backend URL.

### In Development: Two Servers, Two Origins

In development, `vite dev` serves the front on port 5173 and the API remains on the local Dolibarr. Two origins, therefore:
- `api.php/manifest.webmanifest` is not found on port 5173, since nothing serves `api.php` at that address
- pointing the link to `https://dolibarr.local/custom/mymodule/pwa/api.php/manifest.webmanifest` causes the manifest to be rejected due to different origin

The solution is to proxy the API from the Vite development server, so that everything is served from `localhost:5173`.

**With the SmartBoot skeleton, there is nothing to code**: the proxy is already in place and only waits for an environment variable.

```bash
# mobile/.env
# Directory that CONTAINS api.php, no trailing slash
VITE_DEV_PROXY_TARGET=https://dolibarr.local/custom/mymodule/pwa
```

If the variable is absent or empty, no proxy is registered and the development server behavior is unchanged.

For a project not from the skeleton, the equivalent configuration:

```javascript
// vite.config.js
import { defineConfig, loadEnv } from "vite";

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), "");
  const proxyTarget = env.VITE_DEV_PROXY_TARGET;

  return {
    server: proxyTarget ? {
      proxy: {
        '/api.php': {
          target: proxyTarget,
          changeOrigin: true,
          secure: false,
        },
      },
    } : undefined,
    // ...
  };
});
```

### In Shared Hosting: DoliProxy

For deployments where the PWA is not served by the client's Dolibarr, DoliProxy provides a `hosted_pwa` mode: the client accesses `{client}.{module}.doliproxy.fr`, the proxy serves the PWA static files on `/` and proxies `/api/*` to the client's Dolibarr. A single origin, so neither CORS nor manifest problems.

The PWA then reads its `config.json` on startup, which contains `"apiUrl": "/api/"`. It never knows the real backend URL.

### And the Preview Mode?

`npm run build` then `npm run preview` serves the build on port 4173. This is useful but partial:

| What preview allows to validate | What it does not allow |
| --- | --- |
| Service Worker registration | dynamic manifest (no `api.php`) |
| precache content | real API calls |
| offline navigation fallback | icons served by SmartAuth |

Complete validation is done on the PWA deployed in `pwa/` and served by Dolibarr.

## Starting Offline

Making an already loaded application work without a network is easy. Making it **start** without a network requires four conditions to be met.

### 1. The Bundle is Fully Precached

`globPatterns` must cover all file types of the build:

```javascript
globPatterns: ['**/*.{js,css,html,ico,png,svg,json}']
```

The `json` extension is not decorative: it precaches translation files from `public/locales/`. Without them, the application starts offline but displays raw translation keys.

### 2. Size Limit is Sufficient

```javascript
maximumFileSizeToCacheInBytes: 3000000
```

> [!WARNING]
> Any file larger than this limit is **silently excluded** from precache. If the main bundle exceeds the limit, the application is unusable offline and nothing signals it at build time. capfullpos encountered exactly this case and had to raise the limit to 5 MB. Check the size of your assets in `pwa/assets/` and keep margin.

### 3. Navigation Fallback is in Place

In `generateSW`, it is implicit. In `injectManifest`, it must be written (see above). Without it, an offline reload on a route other than root gives a blank screen.

### 4. Business Data is in IndexedDB

The Service Worker caches **files**, not data. Business data must be in local database via SmartCommon's `Db` class and `useDb<Feature>` hooks. See [Data Storage](/front/stockage-de-donnees) and [Offline Synchronization](/front/synchronisation).

### Detect Connection State

```javascript
import { useOnlineStatus } from '@cap-rel/smartcommon';

const MyComponent = () => {
  const {
    isOnline,           // true if browser is online
    isServerReachable,  // true/false/null based on health check
    lastOnline,         // timestamp of last connection
    checkNow,           // manual check
  } = useOnlineStatus({
    healthCheckUrl: '/api/health',
    healthCheckInterval: 30000,
    stabilityDelay: 2000,
    timeout: 5000,
  });

  return !isOnline ? <div>Offline mode</div> : null;
};
```

### Synchronization

```javascript
import { useSyncClient } from '@cap-rel/smartcommon';

const sync = useSyncClient({
  apiUrl: import.meta.env.VITE_API_URL,
  getAccessToken: () => api.user?.accessToken,
  scope: ['items'],
  autoSync: true,
  syncInterval: 60000,
});

await sync.create('items', { label: 'Nouveau' });
await sync.sync(); // push + pull

console.log(sync.pendingCount);  // pending operations
console.log(sync.isSyncing);     // sync in progress
```

See [Offline Synchronization](/front/synchronisation) for details, including conflict resolution.

## Application Update

### registerType

| Value | Behavior |
| --- | --- |
| `autoUpdate` | new Service Worker takes over without asking |
| `prompt` | new Service Worker waits for explicit action |

`autoUpdate` assumes that the Service Worker calls `skipWaiting()` and `clients.claim()`. In `injectManifest` mode, it's up to you to write them (section 5 of the `sw.js` skeleton above).

### usePWAUpdate

```javascript
import { usePWAUpdate } from '@cap-rel/smartcommon';

const MyApp = () => {
  const {
    updateAvailable,    // true when an update is ready
    updateActivated,    // true when the update is activated
    checkForUpdates,    // manual check
    applyUpdate,        // apply (skip waiting + reload)
    reloadPage,         // reload page
  } = usePWAUpdate({
    autoReload: false,
    checkInterval: 0,
    onUpdateAvailable: () => {},
    onUpdateActivated: () => {},
  });

  return updateAvailable ? <button onClick={applyUpdate}>Update</button> : null;
};
```

### UpdatePrompt

Ready-to-use component. Three variants:

| Variant | Description |
| --- | --- |
| `toast` | notification at bottom of screen (default) |
| `banner` | fixed banner at top or bottom |
| `modal` | centered modal window |

```javascript
import { UpdatePrompt } from '@cap-rel/smartcommon';

const App = () => (
  <Provider config={appConfig}>
    <UpdatePrompt
      variant="toast"
      checkInterval={60000}
      labels={{
        title: "Update available",
        message: "A new version is available.",
        reloadButton: "Refresh",
        dismissButton: "Later",
      }}
    />
    <Router />
  </Provider>
);
```

The `Provider` also accepts a `pwaUpdate` prop that automatically mounts `UpdatePrompt`. See [Provider Configuration](/front/configuration).

### Display Running Version

SmartMaker convention: each build carries an incremental number, injected by the Makefile in `VITE_APP_VERSION` in the format `<module version>.<build number>` (e.g., `2.0.1.42`, suffixed `-dev` for a debug build).

```javascript
// mobile/src/utils/constants/vite.js
export const APP_VERSION = import.meta.env.VITE_APP_VERSION;
```

Display discreetly at the bottom of the login page, in settings and in the `AboutModal`. This is the first element to ask a user who reports unexpected behavior.

### Update and Data Pending Synchronization

A frequently asked question: what happens to operations done offline when the Service Worker updates and the page reloads?

They survive. The queue is not in the Service Worker cache: `useSyncClient` persists it in a dedicated IndexedDB database (`smartauth_sync`), with its tables `pending_changes`, `pending_conflicts` and `local_tombstones`. Clearing the Service Worker cache or activating a new version does not touch it.

> [!WARNING]
> What destroys the queue are actions that erase site data: "Clear site data" in DevTools, deleting browser data, uninstalling the PWA. Before advising any of these manipulations to a user for troubleshooting, check `sync.pendingCount`.

### Diagnosing a Client Stuck on an Old Version

- DevTools, **Application > Service Workers**: a Service Worker in `waiting` state signals an update ready but not activated
- Check that `cleanupOutdatedCaches()` is indeed called, otherwise old caches accumulate
- As a last resort, **Unregister** then reload with cache clearing

## Offering Installation

Since SmartCommon 1.0.379, the `useInstallPrompt` hook and `InstallPrompt` component handle installation detection and invitation to install.

```javascript
import { InstallPrompt } from "@cap-rel/smartcommon";

<InstallPrompt
  labels={{
    title: t("installPrompt.title"),
    message: t("installPrompt.message"),
    installButton: t("installPrompt.install"),
    dismissButton: t("installPrompt.later"),
    gotItButton: t("installPrompt.gotIt"),
  }}
/>
```

Mount once, high in the tree, next to `<UpdatePrompt />` and inside the API provider. The component renders nothing until there is something to offer.

> [!WARNING]
> Unlike most SmartCommon components, `InstallPrompt` **has no translation bundle**: its default labels only exist in English and `locales.fr.InstallPrompt` does not exist. A French-speaking application must therefore pass its own `labels`. The button keys are `installButton`, `dismissButton` and `gotItButton`.

Some realities to know:
- On iOS, `beforeinstallprompt` does not exist and there is no native prompt: only manual instructions are possible
- "session is not in standalone mode" does not mean "application is not installed"
- `navigator.getInstalledRelatedApps()` only responds if the manifest declares `related_applications` pointing to itself. The SmartAuth dynamic manifest does this; with a static manifest, it's up to you to write it

The complete detail is in the internal documentation `PWA_INSTALL.md`.

## Build and Deployment

```bash
# Production build
npm run build

# Preview the build
npm run preview

# The build generates:
# - dist/index.html
# - dist/assets/*.js
# - dist/assets/*.css
# - dist/sw.js (Service Worker)
```

Deployment in the Dolibarr module:

```bash
cd mobile
npm run build
cp -r dist/* ../pwa/
```

or, with the module Makefile:

```bash
make pwa
```

The `make pwa` target does more than a `npm run build`: it increments the build number, forces `VITE_API_URL` to be relative, derives the list of languages from subdirectories of `public/locales/`, then restores the development `.env`.

## Verify a PWA

### In DevTools
- **Application > Manifest**: the manifest is loaded, no origin error, and icons display
- **Application > Service Workers**: the Service Worker is `activated and running`
- **Application > Cache Storage**: the precache contains the bundle, `index.html` and files from `locales/`

### Test Offline Startup

This is the test that matters, and it must be done in this order:

1. Load the application online, wait for Service Worker to be active
2. Navigate to an internal page, e.g., `/interventions`
3. Check **Offline** in the Network tab
4. **Reload the page** (not just navigate)

If a blank screen appears at this step, the navigation fallback is missing.

### Lighthouse

Lighthouse tab in DevTools, Progressive Web App category. Expected criteria: HTTPS (or localhost), valid manifest with icons, Service Worker registered, offline functionality, responsive design.

## Known Pitfalls

| Symptom | Cause |
| --- | --- |
| "my VitePWA configuration does nothing" | `workbox` options used in `injectManifest` mode |
| blank screen on offline reload | `NavigationRoute` missing from `sw.js` |
| manifest not found 404 | link to `api.php/manifest` instead of `api.php/manifest.webmanifest` |
| "manifest must have the same origin" | front and API on two origins, no Vite proxy |
| application unusable offline without error | bundle larger than `maximumFileSizeToCacheInBytes` |
| raw translation keys offline | `json` missing from `globPatterns` |
| Service Worker registered twice | `injectRegister: "auto"` and manual call to `registerSW` |
| manifest ignored or inconsistent | `<link rel="manifest">` kept with static Vite manifest |

## See Also
- [Provider Configuration](/front/configuration)
- [Data Storage](/front/stockage-de-donnees)
- [Offline Synchronization](/front/synchronisation)
- [SmartBoot Installation](/howto/smartboot)
- [Vite-PWA](https://vite-pwa-org.netlify.app/)
- [Web app manifests (MDN)](https://developer.mozilla.org/fr/docs/Web/Progressive_web_apps/Manifest)
