Back to path
AdvancedPulse · Project 10 of 12 ~7h· 5 milestones

Make it work offline as an installable app

Continues from the last build: A well-tested, well-instrumented Pulse survives every unit and e2e run in CI, but hand someone a train tunnel or a hotel wifi and the metrics dashboard just spins, because every screen assumes the network is always there.

Last rung you locked Pulse down with unit tests, component tests, e2e flows, and an a11y gate in CI, so you trust every green checkmark.

vite-plugin-pwa configurationWeb App Manifest authoringworkbox caching strategiesstale-while-revalidate for API readsservice worker lifecycle and updatesoffline UX and fallback routingcache expiration and versioningdiscriminated-union UI state for async flows

What you'll build

Pulse installs like a native app, boots instantly offline from a precached shell, serves last-known metrics and events from cache the moment the network drops, shows a branded offline page only for the handful of routes it genuinely cannot serve, and tells the user in-app when a new version is ready instead of relying on a silent background swap or a manual hard refresh.

See how we teach, before you sign up

You don't just get code dumped on you. Every starter file and every solution is explained line-by-line, in plain English. Here's one real file from this project:

vite.config.tsts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react(),
    // TODO(rung 10): add VitePWA({ ... }) here with:
    //  - manifest: false (we ship public/manifest.webmanifest ourselves)
    //  - workbox.globPatterns for the precached app shell
    //  - workbox.runtimeCaching for GET /api/* reads (StaleWhileRevalidate)
    //  - workbox.navigateFallback for the offline route
    //  - registerType: 'prompt' so updates never swap silently
  ],
  build: {
    outDir: 'dist',
  },
});

Reading this file

  • react(),The existing React plugin stays first; VitePWA is additive and goes in the same plugins array.
  • manifest: false (we ship public/manifest.webmanifest ourselves)Two valid patterns exist: let the plugin generate the manifest inline, or point it at a static file. This starter picks the static file so the manifest is easy to inspect and lint on its own.
  • workbox.runtimeCaching for GET /api/* reads (StaleWhileRevalidate)This is the exact strategy milestone 3 wires up, only GET reads, never the POST /api/segments write.
  • registerType: 'prompt'The default 'autoUpdate' swaps the service worker silently in the background, which is exactly how teams end up needing a hard refresh to see a fix; 'prompt' is what milestone 5 depends on.

Entry point for the PWA plugin, empty of any offline config until this rung fills it in.

That's 1 of 7 explained code blocks in this single project.

The build, milestone by milestone

  1. 1

    Install vite-plugin-pwa and ship an installable manifest

    4 guided steps

    Without a valid manifest served over HTTPS with a registered service worker, the browser has nothing to base an install prompt on, Pulse stays a tab that disappears the moment the connection drops, exactly the pain this rung exists to fix.

  2. 2

    Precache the app shell so the UI itself never needs the network

    4 guided steps

    If only data is cached and the shell itself is fetched fresh every time, a dead connection means the React app never even boots, precaching the shell is what makes the rest of this rung possible.

  3. 3

    Stale-while-revalidate the API reads, with real expiration

    4 guided steps

    A dashboard that spins until the network answers feels broken the moment a connection blips, stale-while-revalidate means the user always sees last-known numbers immediately, with a fresher copy swapping in a moment later if the network cooperates.

  4. 4

    Build a branded offline fallback route

    4 guided steps

    An unvisited deep link on a dead connection has nothing cached to fall back to, without a dedicated offline route the browser shows its own dinosaur-style error page, which instantly breaks the illusion that Pulse is an app rather than a website.

  5. 5

    Prompt for updates instead of forcing a hard refresh

    4 guided steps

    The default service worker lifecycle keeps a new version waiting in the background until every tab closes, teams without an explicit update prompt end up telling users to hard-refresh or close all tabs to see a fix, which nobody knows to do.

What's inside when you start

2 starter files, ready to clone
5 guided milestones
5 full reference solutions
7 code blocks explained line-by-line
5 "is it working?" checks
4 interview questions it prepares you for

You'll walk away with

vite-plugin-pwa configured with a real manifest, a maskable icon set, and registerType 'prompt'
A precached app shell that renders fully offline on reload, with cleanupOutdatedCaches wired so old versions never linger
A stale-while-revalidate runtime cache for GET /api/metrics, /api/events, and /api/segments with expiration and cacheable-response guards
A branded offline fallback route precached for navigation requests that cannot otherwise be served
An update-available banner backed by a single SWStatus union, with a working one-click reload

This is portfolio-grade. Build it free.

Sign up to unlock every milestone step-by-step, the code skeletons, full reference solutions, and checkable tasks, with your progress saved as you build.

Start building