Skip to content

Progressive Web App

Paulo Gomes da Cruz Junior edited this page Jun 16, 2026 · 4 revisions

Progressive Web App (PWA) Guide

What is a Progressive Web App?

A Progressive Web App (PWA) is a type of application software delivered through the web, built using common web technologies including HTML, CSS, and JavaScript. PWAs are intended to work on any platform that uses a standards-compliant browser, including both desktop and mobile devices. What sets PWAs apart is their ability to provide a user experience similar to native apps, including offline support, background sync, push notifications, and the ability to be installed on a user's device.

PWAs combine the best of web and native apps. Like websites, they are accessible from anywhere and can be updated instantly. Like native apps, they can be installed, work offline, and integrate with device features. This is achieved through technologies such as service workers, web app manifests, and caching strategies. PWAs are discoverable, linkable, and can be shared via URL, but once installed, they appear and behave like any other app on the device.

The core features of a PWA include:

  • Installability: Users can add the app to their home screen or desktop.
  • Offline Capability: PWAs can function without a network connection, thanks to service workers and caching.
  • App-like Experience: They launch in their own window, support push notifications, and can access device hardware.
  • Progressiveness: They work for every user, regardless of browser choice, because they are built with progressive enhancement as a core tenet.

For a deeper dive, see:

Required Dependencies

To enable PWA features in this project, we installed the following dependencies:

Runtime dependencies:

  • idb: A small library for IndexedDB, used for robust client-side storage and caching, which is essential for offline support in PWAs.
  • luxon: A powerful library for working with dates and times. We use Luxon to handle time-to-live (TTL) and expiration logic for cached data, ensuring accurate and reliable time calculations across browsers and time zones.

Development dependencies:

  • @types/luxon: TypeScript types for Luxon.
  • vite-plugin-pwa: A Vite plugin that streamlines the process of adding PWA support, including service worker registration and manifest generation.
  • @types/service-worker-mock: TypeScript types for mocking service workers in tests.

To install all required dependencies, run:

npm install idb luxon
npm install -D @types/luxon vite-plugin-pwa @types/service-worker-mock

Vite PWA Configuration

To enable PWA support, we use the vite-plugin-pwa plugin in our vite.config.ts file. This plugin handles service worker registration, manifest generation, and development-time features. Here are the key configuration options and what they do:

VitePWA({
 registerType: 'autoUpdate', // Automatically update the service worker when a new version is available
 strategies: 'injectManifest', // Use a custom service worker (src/service-worker.ts)
 srcDir: 'src',
 filename: 'service-worker.ts',
 injectRegister: 'auto', // Automatically inject the registration script
 devOptions: {
  enabled: true,
  type: 'module', // Use module type for service worker in development
 },
 workbox: {
  navigateFallback: '/index.html',
  globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
  sourcemap: true,
 },
 manifest: {
  id: '/',
  lang: 'en',
  short_name: 'Application Name',
  name: 'Application Name',
  start_url: '.',
  display: 'standalone',
  theme_color: '#000000',
  background_color: '#ffffff',
  description: 'Application description.',
  icons: [
   { src: '/icons/favicon.ico', sizes: '48x48', type: 'image/x-icon' },
   { src: '/icons/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
   { src: '/icons/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
   { src: '/icons/favicon-64x64.png', sizes: '64x64', type: 'image/png' },
   { src: '/icons/android-chrome-192x192.png', sizes: '192x192', type: 'image/png' },
   { src: '/icons/android-chrome-512x512.png', sizes: '512x512', type: 'image/png' },
  ],
  screenshots: [
   { src: '/screenshots/homepage.jpeg', sizes: '1539x1054', type: 'image/jpeg', form_factor: 'wide' }, //Include as many screenshots as needed
   { src: '/screenshots/mobile-homepage.jpeg', sizes: '375x667', type: 'image/jpeg' },
  ],
  display_override: ['window-controls-overlay', 'standalone'],
 },
})

Manifest

The manifest section is crucial for defining how your PWA appears and behaves when installed. It includes:

  • id, name, short_name: Identifiers and display names for your app.
  • start_url: The URL the app loads when launched.
  • display: 'standalone' makes the app look and feel like a native app.
  • theme_color, background_color: Colors for the app's UI and splash screen.
  • description: A human-readable description of your app.
  • icons: Various icon sizes for different devices and platforms.
  • screenshots: Images to showcase your app in installation prompts or app stores.
  • display_override: Allows for advanced display modes, such as window controls overlay.

Setting Up the Service Worker

To enable offline support and background updates, you need to register a service worker in your application. This is typically done by creating a registerServiceWorker.ts file and invoking it from your main entry point (main.tsx).

1. Create registerServiceWorker.ts

Create a file at src/registerServiceWorker.ts with the following content:

import { registerSW } from 'virtual:pwa-register';
import { sendToastEvent } from '@/hooks/useNotificationEvents/eventHandler';

const updateSW = registerSW({
  onRegistered() {
    sendToastEvent({ eventType: 'success', title: 'App ready', description: 'Ready to work offline.' });
  },
  onRegisterError(error) {
    sendToastEvent({ eventType: 'error', title: 'Registration failed', description: JSON.stringify(error) });
  },
  onNeedRefresh() {
    sendToastEvent({ eventType: 'info', title: 'Update available', description: 'Refresh to get the latest version.' });
  },
  onOfflineReady() {
    sendToastEvent({ eventType: 'info', title: 'You are offline', description: 'Offline content is still available.' });
  },
});

export default updateSW;

What does each registerSW callback do?

  • onRegistered: Called when the service worker is successfully registered. We can notify the user that offline support is ready.
  • onRegisterError: Called if registration fails. This is a good place to surface an error notification.
  • onNeedRefresh: Called when a new version of the app is available. You can prompt the user to refresh for the latest content.
  • onOfflineReady: Called when the app is ready to work offline (all assets are cached).

2. Call the Service Worker Registration in main.tsx

Import and call the exported function from your main entry point, typically at the top of src/main.tsx:

import updateSW from '@/registerServiceWorker';

updateSW();

This ensures the service worker is registered as soon as your app loads.

3. How it Works

  • The registerSW function (provided by vite-plugin-pwa) handles the service worker lifecycle.
  • The callbacks allow you to provide user feedback for registration, errors, updates, and offline readiness.
  • By calling updateSW() in your main file, you ensure the service worker is always registered and up-to-date.

For more details, see the vite-plugin-pwa documentation.

Custom Service Worker File (Current Version)

This project uses a custom service-worker.ts file, which overrides the default service worker generated by the Vite PWA plugin. This gives you full control over offline support, asset caching, and background sync.

The file is organized into several logical blocks:

1. Initialization and Claims

  • Calls precacheAndRoute(self.__WB_MANIFEST) to precache all assets generated by the build process. This ensures your app shell and static assets are available offline.
  • Calls self.skipWaiting() and clientsClaim() to immediately activate the new service worker and take control of all clients as soon as it's installed.

2. Asset Caching

  • Registers a route for static assets (JavaScript, CSS, images) using a StaleWhileRevalidate strategy. This means assets are served quickly from cache and updated in the background for freshness.
registerRoute(
  ({ request }) =>
    request.destination === 'script' ||
    request.destination === 'style' ||
    request.destination === 'image',
  new StaleWhileRevalidate(),
);

3. Background Sync and Periodic Sync Event Listeners

  • Adds an event listener for sync events. When a background sync with the tag 'sync-mutation' is triggered, it calls your custom handleSyncMutation() logic.
  • Adds an event listener for periodicsync events. When a periodic sync occurs, it calls your custom handleSyncRefresh() logic with the event tag.
self.addEventListener('sync', (event) => {
  const syncEvent = event as SyncEvent;
  if (syncEvent.tag === 'sync-mutation') {
    syncEvent.waitUntil(handleSyncMutation());
  }
});

self.addEventListener('periodicsync', (event) => {
  const syncEvent = event as PeriodicSyncEvent;
  syncEvent.waitUntil(handleSyncRefresh(syncEvent.tag));
});

Summary:

  • By overriding the default service worker, you gain full control over offline asset caching and background sync.
  • The file is modular, with each block responsible for a specific aspect of PWA behavior.
  • You can further customize these blocks to fit your app’s needs, such as adding new caching strategies, handling push notifications, or supporting additional offline logic.

For more details on customizing your service worker, see the Workbox documentation and the vite-plugin-pwa guide.

IndexedDB, Offline Caching, and Middleware

IndexedDB is a low-level, client-side storage technology built into all modern browsers. It allows web applications to store large amounts of structured data (including files/blobs) directly in the user's browser, making it ideal for offline-first applications, caching, and complex data management scenarios. Unlike localStorage or sessionStorage, IndexedDB is asynchronous, supports transactions, and can efficiently store and query large datasets using indexes and key ranges.

Key features of IndexedDB:

  • Stores data as key-value pairs in object stores, similar to tables in a database.
  • Supports complex data types, including objects, arrays, and binary data.
  • Allows for efficient searching and filtering using indexes.
  • Works asynchronously, so it doesn't block the main thread.
  • Enables offline access, background sync, and advanced caching strategies for PWAs.
  • Data persists even after the browser is closed or the device is offline.

Use cases:

  • Caching API responses for offline access
  • Storing user-generated content or queued mutations while offline
  • Saving large datasets for fast, local querying
  • Enabling background sync and deferred actions

Learn more:

This section covers how the app uses IndexedDB (via the idb dependency) for offline data and mutation caching, the configuration and utility functions, and the middleware that integrates these features into your API layer.

1. IndexedDB and the idb Dependency

We use the idb library for a simple, promise-based interface to IndexedDB. This allows robust, client-side storage for offline data and queued mutations.

Configuration (src/config/pwa/idb/config.ts)

The configuration file provides helpers to:

  • Open and upgrade the database
  • Add, retrieve, and delete offline cache items
  • Add and retrieve queued mutations
  • Run custom transactions

Key functions:

  • addOfflineItem(key, value, version?): Store a value in the offline cache.
  • getOfflineItem(key, expectedVersion?, ttl?): Retrieve a value, with optional version and TTL checks. Throws if not found, expired, or version mismatch.
  • deleteOfflineItem(key): Remove a cached item.
  • getAllOfflineItems(): Get all cached items.
  • addMutation(value): Queue a mutation for later sync.
  • getAllPendingMutations(): Get all queued mutations.
  • runMutationTransaction(mode, callback): Run a custom transaction on the mutation queue.

Custom error classes like NoOfflineItemError and CacheVersionMismatchError help with error handling and flow control.

2. Middleware for Offline Caching and Mutations

The middleware layer integrates IndexedDB caching into your API requests. For a full overview of the middleware concept, see Requests and APIs.

offlineDataMiddleware

  • Used for GET/data requests.
  • When idbSave is enabled, successful responses are cached in IndexedDB.
  • On failure (when offline), attempts to return cached data instead of failing.
  • Handles versioning and TTL if configured.
  • Notifies the user when data is served from cache.

offlineMutationMiddleware

  • Used for mutational requests (POST, PUT, PATCH, DELETE).
  • When idbSave is enabled and offline, mutation requests are queued in IndexedDB for later replay.
  • Returns a 204 response to indicate the mutation was queued, not sent.
  • Ensures data consistency and enables true offline-first workflows.

3. Utilities (src/config/pwa/utils.ts)

Utility functions help manage background sync and periodic sync registration:

  • registerBackgroundSync(tag): Registers a background sync event for a given tag, if supported and permitted.
  • registerPeriodicSync(tag, minInterval): Registers a periodic sync event for a given tag and interval, if supported and permitted.

These utilities are used by the middleware and other PWA features to ensure data is kept in sync and mutations are replayed when connectivity is restored.


Summary:

  • IndexedDB (via idb) is used for robust offline caching and mutation queuing.
  • Middleware integrates these features into your API layer for seamless offline support.
  • Utilities help manage background and periodic sync, further enhancing the offline experience.
  • For more on middleware, see Requests and APIs.

Additional PWA-Related Features and Changes

TypeScript Configuration for Web Workers

To support service workers and background sync, you must add "WebWorker" and "WebWorker.Iterable" to the types array in your tsconfig.json. This ensures TypeScript recognizes service worker globals and APIs.

How to add:

{
  "compilerOptions": {
    // ...existing options...
    "types": [
      "WebWorker",
      "WebWorker.Iterable"
      // ...other types...
    ]
  }
}

Why:
This enables proper type checking and autocompletion for service worker and background sync code, preventing errors and improving developer experience.

References:


Notification Event System: useNotificationEvents Hook

App-wide notifications — including all PWA lifecycle messages — are published through the useNotificationEvents hook and its companion helpers.

  • sendToastEvent — always renders as a Carbon toast (used by service workers).
  • sendInlineEvent — always renders as an inline page banner.
  • sendEvent — generic helper when mode must be set dynamically.

Service workers and other non-React publishers import directly from the helper module:

import { sendToastEvent } from '@/hooks/useNotificationEvents/eventHandler';

sendToastEvent({ eventType: 'info', title: 'Offline ready', description: 'You can use the app offline.' });

Inside React components, use the hook:

const { sendToastEvent } = useNotificationEvents();
sendToastEvent({ eventType: 'success', title: 'Saved!', description: 'Your changes were saved.' });

See the Notification Events architecture page for full API details and design constraints.


Offline Detection: useOfflineMode Hook

The useOfflineMode hook provides a simple way to detect and react to online/offline status changes in your app.

  • Exposes the current online status and allows components to respond to connectivity changes.
  • Useful for disabling features, showing offline banners, or triggering background sync.

See the hook-offlinemode documentation for details and usage examples.


Toast Notifications: Notification Provider

The Notification Provider centralizes toast notification logic for your app.

  • Wrap your app with the provider to enable notifications anywhere.
  • Use the useNotification hook to display success, error, info, or warning toasts.
  • Ensures a consistent, user-friendly notification experience.

Example:

const { display } = useNotification();
display({ title: 'Saved!', kind: 'success', timeout: 3000 });

See the context-notification documentation for more.


Background Sync Touchpoint: syncHandlers.ts

The syncHandlers.ts file in src/config/pwa/ serves as an extension point for background sync and periodic sync logic.

  • Exports handleSyncMutation and handleSyncRefresh functions, which are called by the service worker during sync events.
  • By default, these functions are empty—developers can implement custom logic for replaying mutations or refreshing data as needed.

Example:

export const handleSyncMutation = async () => {
  // Implement mutation replay logic here
};

export const handleSyncRefresh = async (identifier: string) => {
  // Implement refresh logic here
};

This design allows for flexible, app-specific background sync strategies.

Waste Plus Wiki

Home

Architecture

Codes

Decisions

Knowledge Base

Development

Changes

Clone this wiki locally