Make an AnalogJS app a PWA with Angular Service Worker (NGSW)

December 27, 2025 Updated: January 1, 2026 Rens Jaspers Angular AnalogJS PWA

Tested with: Analog v2, Angular v21, Vite v7.

I almost always add Angular Service Worker (NGSW) to my projects.

It gives you offline caching, fast repeat visits, a controlled update flow, and predictable asset caching. And it adds almost no complexity, because NGSW does the heavy lifting for you.

In a regular Angular CLI project you do:

ng add @angular/pwa

And you’re basically done.

With AnalogJS, it looks like the same approach works… but it’s missing one important build step.

The problem I hit

After running ng add @angular/pwa I had:

  • ngsw-config.json
  • public/manifest.webmanifest
  • provideServiceWorker('ngsw-worker.js', ...) in the app config

So far so good.

But in the browser I ran into a weird error while testing the service worker setup.

The root cause: the service worker output files were not in the build output.

Specifically:

  • ngsw-worker.js (the service worker script)
  • ngsw.json (the generated manifest the worker needs)

Because those files were missing, the app (or the SW) would request them and the server would often return index.html instead. That usually shows up as a cryptic runtime error like “unexpected token <” (because HTML was returned where JSON/JS was expected).

So the install step was fine, the runtime wiring was fine… but the build output was incomplete.

Why this happens in AnalogJS

AnalogJS builds with Vite.

In a classic Angular CLI build, Angular runs an extra step that takes your ngsw-config.json and augments the output with the generated NGSW artifacts.

In AnalogJS, that step does not automatically happen.

We found a GitHub thread about this and Brandon Roberts shared a small demo repo with the missing piece. Below is the extracted “essentials only” version.

Step 1 — Enable service worker in angular.json

Make sure your Analog build target has these options:

{
  "serviceWorker": true,
  "ngswConfigPath": "ngsw-config.json"
}

This is important because the build augmentation reads these options.

Step 2 — Register NGSW in your app config

In Angular (standalone) you register the worker like this:

import { isDevMode } from '@angular/core';
import { provideServiceWorker } from '@angular/service-worker';

// ...
provideServiceWorker('ngsw-worker.js', {
  enabled: !isDevMode(),
  registrationStrategy: 'registerWhenStable:30000',
}),

Two notes:

  • In dev (npm run dev) it’s common to disable the SW so it doesn’t fight HMR and caching.
  • Always verify the SW on a production build (more on that below).

Step 3 — The missing piece: a Vite plugin that generates NGSW output

This is the key part.

We add a small Vite plugin that runs after the client build completes and asks Angular to generate the service worker artifacts into dist/client.

This is basically what a classic Angular build would do for you.

import { augmentAppWithServiceWorker } from "@angular/build/private";
import { existsSync } from "node:fs";
import * as path from "path";
import { Plugin } from "vite";

function swBuildPlugin(): Plugin {
  let swBuilt = false;
  const clientOutputDir = path.join(process.cwd(), "dist/client");
  const clientIndexHtml = path.join(clientOutputDir, "index.html");

  return {
    name: "analog-sw",
    async closeBundle() {
      // Only build once, and only when the client build output exists
      if (swBuilt || !existsSync(clientIndexHtml)) {
        return;
      }

      swBuilt = true;
      await augmentAppWithServiceWorker(".", process.cwd(), clientOutputDir, "/");
    },
  };
}

Then you include it in your plugins list (together with analog(...)):

plugins: [analog(/* ... */), swBuildPlugin()];

Once this runs, you’ll see ngsw-worker.js and ngsw.json appear in dist/client.

That was the missing link for me: installing and registering NGSW was not enough. It also needs a build-time step to generate the files.

Step 4 — Production test (don’t trust dev mode)

To test it properly:

npm run build
npm run preview

Then open your site and check:

  • Application tab → Service Workers (Chrome)
  • Application tab → Manifest

Important — Prefetch Vite chunks under /assets

Analog (via Vite) typically outputs your JS chunks under /assets/, e.g. /assets/index.page-.js.

That matters because a common starting point like this:

{
  "name": "app",
  "installMode": "prefetch",
  "resources": {
    "files": ["/*.js", "/*.css"]
  }
}

only matches files in the root (like /ngsw-worker.js), not /assets/*.js.

If you want your route modules / lazy chunks to be available immediately (and stay available across a deployment even after a refresh), extend the app asset group to include:

{
  "name": "app",
  "installMode": "prefetch",
  "resources": {
    "files": ["/*.css", "/*.js", "/assets/*.js", "/assets/*.css"]
  }
}

This avoids a nasty class of “chunk load” errors where the browser requests a missing JS file (old hash) and gets HTML back (or a 404), because the needed chunk wasn’t in the service worker cache yet.

Wrap-up

You CAN add the NGSW to your AnalogJS project just fine.

The “gotcha” with AnalogJS is not the runtime code. It’s the build output: your SW files must be generated into the Vite output folder.

Once you add that tiny Vite plugin, it works like a charm.

Sources