---
title: Configuring PWA
related:
  - title: Configuring quasar.config file
    path: ../quasar-config-file.md
---
## Service Worker

Adding PWA mode to a Quasar project means a new folder will be created: `/src-pwa`, which contains PWA specific files:

- **src-pwa**
  - **register-sw.js**
    _(or .ts) UI code *managing* service worker (main thread)_
  - **manifest.json**
    _Your PWA manifest file_
  - **package.json**
    _helps install PWA only deps directly under /src-pwa_
  - **sw**
    _Service worker context (WebWorker)_
    - **custom-sw.js**
      _(or .ts) Optional custom service worker file (InjectManifest mode ONLY)_
    - **tsconfig.json**
      _TypeScript only - WebWorker lib, scoped to /src-pwa/sw/_

You can freely edit these files. Notice a few things:

1. `register-sw.js` is automatically imported into your app (like any other `/src` file). It registers the service worker created by Workbox and lets you respond to its lifecycle events. The generated worker depends on `quasar.config > pwa > workboxMode`.
2. `sw/custom-sw.js` will be your service worker file ONLY if workbox plugin mode is set to "InjectManifest" (quasar.config file > pwa > workboxMode: 'InjectManifest'). Otherwise, Quasar and Workbox will create a service-worker file for you. The `/src-pwa/sw/` folder is the WebWorker context. Anything inside it runs in the service worker, not the main thread.
3. It makes sense to run [Lighthouse](https://developers.google.com/web/tools/lighthouse/) tests on production builds only.

> [!NOTE]
> Read more on `register-sw.js` and how to interact with the Service Worker on [Handling Service Worker](handling-service-worker.md) documentation page.

## quasar.config file

This is the place where you can configure Workbox behavior and also tweak your manifest.json.

```ts
pwa: {
  /**
   * Workbox operating mode.
   * @default 'GenerateSW'
   */
  workboxMode?: "GenerateSW" | "InjectManifest";

  /**
   * Generated service worker filename to use (needs to end with .js)
   * @default sw.js
   */
  swFilename?: string;

  /**
   * PWA manifest filename to use on your browser
   * @default manifest.json
   */
  manifestFilename?: string;

  /**
   * Should you need some dynamic changes to the /src-pwa/manifest.json,
   * use this method to do it.
   */
  extendPWAManifestJson?: (
    json: PwaManifestOptions
  ) => void | PwaManifestOptions | Promise<void | PwaManifestOptions>;

  /**
   * Does the PWA manifest tag requires crossorigin auth?
   * @default false
   */
  useCredentialsForManifestTag?: boolean;

  /**
   * Auto inject the PWA meta tags?
   * If using the function form, return HTML tags as one single string.
   * @default true
   */
  injectPWAMetaTags?: boolean | ((injectParam: InjectPWAMetaTagsParams) => string);

  /**
   * Extend the Rolldown config that is used for the custom service worker
   * (if using it through workboxMode: 'InjectManifest').
   *
   * Can be async. Can directly modify the "config" parameter or
   * return a new one that will be merged with the default one.
   */
  extendPWACustomSWConf?: (
    config: RolldownOptions
  ) => void | RolldownOptions | Promise<void | RolldownOptions>;

  /**
   * Extend/configure the Workbox GenerateSW options.
   *
   * Can be async. Can directly modify the "config" parameter or
   * return a new one that will be merged with the default one.
   */
  extendPWAGenerateSWOptions?: (
    config: GenerateSWOptions
  ) => void | GenerateSWOptions | Promise<void | GenerateSWOptions>;

  /**
   * Extend/configure the Workbox InjectManifest options.
   *
   * Can be async. Can directly modify the "config" parameter or
   * return a new one that will be merged with the default one.
   */
  extendPWAInjectManifestOptions?: (
    config: InjectManifestOptions
  ) => void | InjectManifestOptions | Promise<void | InjectManifestOptions>;

  /**
   * Extend the generated `.quasar/tsconfig.pwa-sw.json` file.
   *
   * NOT async! Can directly modify the "tsConfig" parameter or
   * return a new one that will be merged with the default one.
   */
  extendPWASwTsConfig (tsConfig) {
    tsConfig.compilerOptions!.lib!.push('WebWorker.AsyncIterable')
  }
}

sourceFiles: {
  pwaRegisterServiceWorker: 'src-pwa/register-sw',
  pwaServiceWorker: 'src-pwa/sw/custom-sw',
  pwaManifestFile: 'src-pwa/manifest.json',
}
```

Should you want to tamper with the Vite config for UI in /src:

Example "/quasar.config file":

```js
export default defineConfig(ctx => {
  return {
    build: {
      extendViteConf(viteConf) {
        if (ctx.mode.pwa) {
          // do something with viteConf
          // or return an object to deeply merge with current viteConf
        }
      }
    }
  }
})
```

More information: [Workbox](https://developers.google.com/web/tools/workbox).

## Adding your own meta tags in index.html

Quasar CLI adds (dynamically) some PWA oriented meta tags into your index.html. Should you wish to customize the tags, first disable this behavior in the `/quasar.config` file:

Example "/quasar.config file":

```js
pwa: {
  injectPWAMetaTags: false
}
```

Then, edit your `/index.html` file. The following are the actual meta tags that Quasar CLI injects dynamically:

```html
<head>
  <% if (ctx.mode.pwa) { %>
  <meta name="theme-color" content="<%= pwaManifest.theme_color %>" />
  <link
    rel="mask-icon"
    href="icons/safari-pinned-tab.svg"
    color="<%= pwaManifest.theme_color %>"
  />
  <meta name="mobile-web-app-capable" content="yes" />
  <meta name="apple-mobile-web-app-status-bar-style" content="default" />
  <meta name="apple-mobile-web-app-title" content="<%= pwaManifest.name %>" />
  <link rel="apple-touch-icon" href="icons/apple-icon-120x120.png" />
  <link
    rel="apple-touch-icon"
    sizes="152x152"
    href="icons/apple-icon-152x152.png"
  />
  <link
    rel="apple-touch-icon"
    sizes="167x167"
    href="icons/apple-icon-167x167.png"
  />
  <link
    rel="apple-touch-icon"
    sizes="180x180"
    href="icons/apple-icon-180x180.png"
  />
  <% } %>
</head>
```

Notice that you have access to your PWA manifest through `pwaManifest` above.

Alternatively, you can assign a function to injectPWAMetaTags like below:

Example "/quasar.config file":

```js
pwa: {
  injectPWAMetaTags ({ pwaManifest, publicPath }) {
    return `<meta name="mobile-web-app-capable" content="yes">`
      + `<meta name="apple-mobile-web-app-status-bar-style" content="default">`
  }
}
```

## Picking Workbox mode

There are two Workbox operating modes: **GenerateSW** (default) and **InjectManifest**.

Setting the mode that you want to use is done through the `/quasar.config` file:

Example "/quasar.config file":

```js
pwa: {
  workboxMode: 'GenerateSW',
  extendPWAGenerateSWOptions (cfg) {
    // configure workbox on GenerateSW
  }
}

pwa: {
  workboxMode: 'InjectManifest',
  extendPWAInjectManifestOptions (cfg) {
    // configure workbox on InjectManifest
  }
}
```

### GenerateSW

When to use GenerateSW:

- You want to precache files.
- You have simple runtime configuration needs (e.g. the configuration allows you to define routes and strategies).

When NOT to use GenerateSW:

- You want to use other Service Worker features (i.e. Web Push).
- You want to import additional scripts or add additional logic.

> [!NOTE]
> Please check the available workboxOptions for this mode on [Workbox website](https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-build#.generateSW).

### InjectManifest

When to use InjectManifest:

- You want more control over your service worker.
- You want to precache files.
- You have more complex needs in terms of routing.
- You would like to use your service worker with other APIs (e.g. Web Push).

When NOT to use InjectManifest:

- You want the easiest path to adding a service worker to your site.

> [!IMPORTANT]
>
> - If you want to use this mode, you will have to write the service worker (`/src-pwa/sw/custom-sw.js`) file by yourself.
> - Please check the available workboxOptions for this mode on [Workbox website](https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-build#.injectManifest).

The following snippet is the default code for a custom service worker (`/src-pwa/sw/custom-sw.js`) which mimics the behavior of `generateSW` mode:

Example "/src-pwa/sw/custom-sw file":

```ts
/*
 * This file (which will be your service worker)
 * is picked up by the build system ONLY if
 * quasar.config file > pwa > workboxMode is set to "InjectManifest"
 */

import { clientsClaim } from "workbox-core";
import { NavigationRoute, registerRoute } from "workbox-routing";
import {
  cleanupOutdatedCaches,
  createHandlerBoundToURL,
  precacheAndRoute
} from "workbox-precaching";

declare const self: ServiceWorkerGlobalScope & typeof globalThis;

void self.skipWaiting();
clientsClaim();

// Use with precache injection
precacheAndRoute(self.__WB_MANIFEST);

cleanupOutdatedCaches();

if (import.meta.env.QUASAR_PROD) {
  // Non-SSR/SSG fallbacks to index.html
  // Production SSR/SSG fallbacks to offline.html (except for dev)
  registerRoute(
    new NavigationRoute(
      createHandlerBoundToURL(import.meta.env.QUASAR_PWA_FALLBACK_HTML),
      {
        denylist: [
          new RegExp(import.meta.env.QUASAR_PWA_SERVICE_WORKER_REGEX),
          /workbox-(.)*\.js$/
        ]
      }
    )
  );
}
```

## Configuring Manifest File

The Manifest file is located at `/src-pwa/manifest.json`. You can freely edit it.

Should you need to change it dynamically at build time, you can do so by editing the `/quasar.config` file:

Example "/quasar.config file":

```js
pwa: {
  extendPWAManifestJson (json) {
    // tamper with the json inline
  }
}
```

Please read about the [manifest config](https://developer.mozilla.org/en-US/docs/Web/Manifest) before diving in.

> [!NOTE]
> [!NOTE]
> that you don't need to edit your index.html file (generated from `/index.html`) to link to the manifest file. Quasar CLI takes care of embedding the right things for you.

> [!TIP]
> If your PWA is behind basic auth or requires an Authorization header, set quasar.config file > pwa > useCredentialsForManifestTag to `true` to include `crossorigin="use-credentials"` on the manifest.json meta tag.

This option affects only the manifest request. If you add runtime caching for authenticated API responses, do not use a broad cache rule that can return one user's private response to another session. Limit matching to intended URLs and methods, avoid caching sensitive responses unless the cache is safely partitioned and cleared on sign-out, and respect the server's cache policy.

## PWA Checklist

More info: [PWA Checklist](https://web.dev/pwa-checklist/)

> [!WARNING]
> Do not run [Lighthouse](https://developers.google.com/web/tools/lighthouse/) on your development build because at this stage the code is intentionally not optimized and contains embedded source maps (among many other things). See the [Testing and Auditing](../testing-and-auditing.md) section of these docs for more information.

## Reload & Update Automatically

For those who don't want to manually reload the page when the service worker is updated **and are using the default generateSW workbox mode**, Quasar CLI has configured Workbox to activate it at once. Should you need to disable this behavior:

Example "/quasar.config file":

```js
pwa: {
  extendPWAGenerateSWOptions (cfg) {
    cfg.skipWaiting = false
    cfg.clientsClaim = false
  }
}
```

## Filename hashes quirk

Due to how Rolldown builds the assets (through Vite), when you change any of your script source files (.js) this will also change the hash part of (almost) ALL .js files (ex: `454d87bd` in `assets/index.454d87bd.js`). The revision number of all assets will get changed in your service worker file and this means that when PWA updates it will re-download ALL your assets again.

By default, Vite builds all filenames **with the hash part**. Should you want your filenames to NOT contain it, so that only the changed files get re-downloaded, edit the `/quasar.config` file:

Example "/quasar.config file":

```js
build: {
  useFilenameHashes: false // true by default
}
```

Two things then need your attention:

1. Configure your webserver cache for these files as low as possible (they keep their names across deploys), so that the visitors which don't use the PWA functionality get consistent resources.
2. Safari keeps the scripts that a page preloaded (through `<link rel="modulepreload">`) in an in-memory cache and reuses them across the reload that follows a service worker update, without asking the service worker. With stable filenames the reloaded page then runs the new entry file together with old chunks and fails with `SyntaxError: Importing binding name '...' is not found.`, leaving a blank page on every reload of that tab. So before a page reloads to apply an update, and in every open tab that the new worker takes over, fetch the precached scripts through the new worker; a `fetch()` of a URL evicts its stale entry:

Example "/src-pwa/register-service-worker.js":

```js
navigator.serviceWorker.addEventListener('controllerchange', async () => {
  // (skip this for the very first install, nothing stale can exist yet)
  const urls = []

  for (const name of await caches.keys()) {
    if (!name.includes('precache')) continue

    const cache = await caches.open(name)
    for (const req of await cache.keys()) {
      const url = req.url.split('?')[0]
      if (url.endsWith('.js')) urls.push(url)
    }
  }

  await Promise.allSettled(
    urls.map(url => fetch(url).then(res => res.body?.cancel()))
  )

  window.location.reload()
})
```

The quasar.dev website does this itself (`docs/src-pwa/register-sw.js` in the Quasar repository), with a waiting service worker (`skipWaiting: false`) so that the update is applied only when the user asks for it.
