Skip to main content
Jet is built as a Progressive Web App (PWA), providing native app-like capabilities including offline support, automatic updates, and installability on mobile devices.

Overview

The PWA features are powered by Angular’s Service Worker (@angular/service-worker), which handles caching strategies, update management, and offline functionality.

Service Worker Configuration

The service worker is registered in src/app/app.config.ts:54:
provideServiceWorker('ngsw-worker.js', {
  enabled: !isDevMode(),
  registrationStrategy: 'registerWhenStable:30000',
})
The service worker is only enabled in production mode and registers after the app becomes stable or after 30 seconds, whichever comes first.

Cache Configuration

The caching strategy is defined in ngsw-config.json:
{
  "$schema": "./node_modules/@angular/service-worker/config/schema.json",
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.csr.html",
          "/index.html",
          "/manifest.webmanifest",
          "/*.css",
          "/*.js",
          "/i18n/en.json"
        ]
      }
    },
    {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": ["/**/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"]
      }
    }
  ]
}

Asset Groups

app
AssetGroup
Install Mode: prefetchCritical application files that are downloaded and cached immediately:
  • HTML files (index.html, index.csr.html)
  • JavaScript bundles
  • CSS stylesheets
  • Web manifest
  • Default English translations
  • Favicon
assets
AssetGroup
Install Mode: lazy
Update Mode: prefetch
Media and font files that are cached on-demand:
  • Images (svg, jpg, png, webp, avif, gif)
  • Fonts (otf, ttf, woff, woff2)
  • Cursors (cur)

Web App Manifest

The PWA manifest defines how the app appears when installed (public/manifest.webmanifest):
{
  "name": "Jet - Angular starter-kit for building quality web apps fast. Now with Supabase.",
  "short_name": "Jet",
  "display": "standalone",
  "scope": "./",
  "start_url": "./",
  "icons": [
    {
      "src": "icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable any"
    }
  ]
}
The "purpose": "maskable any" ensures icons display correctly on all platforms, including Android adaptive icons.

Service Worker Service

Jet includes a comprehensive service for managing updates (src/app/services/service-worker/service-worker.service.ts:20):
@Injectable({ providedIn: 'root' })
export class ServiceWorkerService {
  readonly #swUpdate = inject(SwUpdate);
  readonly #translocoService = inject(TranslocoService);
  readonly #alertService = inject(AlertService);
  
  #isUpdateReady: boolean;
  readonly #lastUpdateCheckTimestamp: WritableSignal<string>;

  public constructor() {
    this.#isUpdateReady = false;
    
    const storedLastUpdateCheckTimestamp: null | string =
      this.#storageService.getLocalStorageItem<string>(LocalStorageKey.LastUpdateCheckTimestamp);

    this.#lastUpdateCheckTimestamp = signal(
      storedLastUpdateCheckTimestamp ?? new Date().toISOString(),
    );

    this.#subscribeToVersionUpdates();
  }
}

Automatic Update Detection

The service monitors for new versions and notifies users:
#subscribeToVersionUpdates(): void {
  this.#swUpdate.versionUpdates
    .pipe(takeUntilDestroyed(this.#destroyRef))
    .subscribe((versionEvent: VersionEvent) => {
      switch (versionEvent.type) {
        case 'NO_NEW_VERSION_DETECTED':
          this.#analyticsService.logAnalyticsEvent({ name: 'no_new_version_detected' });
          this.#lastUpdateCheckTimestamp.set(new Date().toISOString());
          break;

        case 'VERSION_DETECTED':
          this.#analyticsService.logAnalyticsEvent({
            data: { version: versionEvent.version.hash },
            name: 'version_detected',
          });
          
          this.#alertService.showAlert(
            this.#translocoService.translate('alerts.downloading-updates'),
          );
          break;

        case 'VERSION_READY':
          this.#isUpdateReady = true;
          
          this.#alertService.showAlert(
            this.#translocoService.translate('alerts.reload-to-update'),
            this.#translocoService.translate('alerts.reload'),
            (): void => {
              window.location.reload();
            },
          );
          break;

        case 'VERSION_INSTALLATION_FAILED':
          this.#loggerService.logError(new Error(versionEvent.error));
          this.#alertService.showErrorAlert(versionEvent.error);
          break;
      }
    });
}

Manual Update Check

Users can manually check for updates from the Settings page (src/app/components/settings-page/settings-page.component.ts:68):
protected async checkForUpdate(): Promise<void> {
  this.#progressBarService.showQueryProgressBar();

  this.#alertService.showAlert(
    this.#translocoService.translate('alerts.checking-for-updates')
  );

  try {
    const isUpdateFoundAndReady: boolean = await this.#serviceWorkerService.checkForUpdate();

    if (!isUpdateFoundAndReady) {
      this.#alertService.showAlert(
        this.#translocoService.translate('alerts.youre-on-the-latest-version'),
      );
    }
  } catch (exception: unknown) {
    if (exception instanceof Error) {
      this.#loggerService.logError(exception);
      this.#alertService.showErrorAlert(exception.message);
    }
  } finally {
    this.#progressBarService.hideProgressBar();
  }
}

Update Flow

1

Version Detection

The service worker detects a new version is available on the server
2

Download Updates

The new version is downloaded in the background while the user continues using the app
3

Notify User

Once the download is complete, a snackbar alert prompts the user to reload
4

Activate New Version

When the user reloads, the new version becomes active immediately

Service Initialization

The service worker service is initialized automatically at app startup:
provideEnvironmentInitializer(() => {
  inject(ServiceWorkerService);
})
The service worker service must be initialized early to properly detect and handle updates.

Offline Support

With the configured caching strategies:
  1. Core app files are cached on first visit
  2. API responses can be cached with custom strategies
  3. Media assets are cached as they’re requested
  4. Translations (English) are prefetched for offline use

Update Tracking

The service tracks the last update check timestamp in localStorage:
effect(
  () => {
    const lastUpdateCheckTimestamp: string = this.#lastUpdateCheckTimestamp();

    untracked(() =>
      this.#storageService.setLocalStorageItem(
        LocalStorageKey.LastUpdateCheckTimestamp,
        lastUpdateCheckTimestamp,
      ),
    );
  },
  { debugName: 'lastUpdateCheckTimestamp' },
);
This timestamp is displayed in the Settings page:
<p>Last checked: {{ lastUpdateCheckTimestamp() | date }}</p>

Analytics Integration

Update events are tracked for analytics:
  • no_new_version_detected - Update check found no updates
  • version_detected - New version detected and downloading
  • version_ready - New version ready to activate
  • version_installation_failed - Update installation failed

Testing PWA Features

Local Testing

ng build
Service workers only run in production mode. Use ng build (not ng serve) to test PWA features.

Chrome DevTools

  1. Open Chrome DevTools
  2. Navigate to Application > Service Workers
  3. Check the service worker status
  4. Test offline mode with the Offline checkbox
  5. View cached resources in Cache Storage

Installability

The app can be installed on:
  • Desktop: Chrome, Edge (“Install App” in address bar)
  • Android: Chrome, Firefox, Samsung Internet (“Add to Home Screen”)
  • iOS/iPadOS: Safari (“Add to Home Screen”)

Installation Prompt

Browsers will show an install prompt when:
  1. The app has a valid manifest
  2. It’s served over HTTPS (or localhost)
  3. It has registered a service worker
  4. The user has engaged with the site

Safe Area Support

Jet handles device safe areas (notches, home indicators) automatically:
.mat-mdc-snack-bar-container {
  margin: calc(8px + env(safe-area-inset-top)) calc(8px + env(safe-area-inset-right))
    calc(8px + env(safe-area-inset-bottom)) calc(8px + env(safe-area-inset-left)) !important;
}

Best Practices

Use API versioning to prevent breaking changes between app updates
Always provide user feedback when updates fail
Verify critical user flows work without network connectivity
Track update success/failure rates to identify deployment issues
Update app icons and metadata when rebranding

Troubleshooting

  • Ensure you’re testing a production build
  • Check the app is served over HTTPS or localhost
  • Verify ngsw-worker.js is accessible
  • Clear the service worker in DevTools
  • Check network requests for ngsw.json
  • Verify the build hash changed in ngsw.json
  • Validate your manifest.webmanifest format
  • Ensure all required manifest fields are present
  • Check icons exist at the specified paths

Next Steps

Theming

Configure Material Design themes

Deployment

Deploy your PWA to production

Build docs developers (and LLMs) love