Swoff Swoff

Client Registration

Register service workers with version checking — framework agnostic.

Prerequisites: Your build script produces versioned SW files and version.json (see Build Scripts). This script registers the generated SW from your app.

Basic Registration

if ("serviceWorker" in navigator) {
  window.addEventListener("load", async () => {
    const manifest = await fetch("/version.json").then((r) => r.json());
    const swUrl = `/sw-v${manifest.version}.js`;
    const registration = await navigator.serviceWorker.register(swUrl);
  });
}

Full Registration with Version Checking

swoff/sw-injector.js
async function checkForUpdate() {
  const response = await fetch("/version.json");
  const manifest = await response.json();
  return manifest;
}

async function registerServiceWorker(version) {
  const swUrl = `/sw-v${version}.js`;
  const registration = await navigator.serviceWorker.register(swUrl);
  localStorage.setItem("swRegisteredVersion", version);
  window.currentSWVersion = version;
  window.swRegisteredVersion = version;
  window.dispatchEvent(new CustomEvent("sw-version-detected"));
  window.dispatchEvent(new CustomEvent("sw-ready"));
  return registration;
}

function shouldRegister() {
  // Add custom preconditions here. Return false to prevent registration.
  return true;
}

async function initServiceWorker() {
  if (!("serviceWorker" in navigator)) return;
  if (!shouldRegister()) return;

  const manifest = await checkForUpdate();
  const currentVersion = localStorage.getItem("swRegisteredVersion");
  window.latestSWVersion = manifest.version;
  window.swMinSupportedVersion = manifest.minSupportedVersion || "0.0.0";

  if (currentVersion === manifest.version) {
    const registration = await navigator.serviceWorker.getRegistration();
    if (registration && registration.active) {
      window.currentSWVersion = currentVersion;
      window.dispatchEvent(new CustomEvent("sw-version-detected"));
      window.dispatchEvent(new CustomEvent("sw-ready"));
    }
  } else if (currentVersion && currentVersion !== manifest.version) {
    window.swAvailableVersion = manifest.version;
    window.swUpdateRequired =
      currentVersion < (manifest.minSupportedVersion || "0.0.0");

    if (autoRegister) {
      const registration = await navigator.serviceWorker.getRegistration();
      if (registration && registration.waiting) {
        if (autoActivate) {
          registration.waiting.postMessage({ type: "SKIP_WAITING" });
        } else {
          window.dispatchEvent(
            new CustomEvent("sw-update-available", {
              detail: { version: manifest.version },
            }),
          );
        }
      } else {
        const newReg = await registerServiceWorker(manifest.version);
        if (autoActivate && newReg.waiting) {
          newReg.waiting.postMessage({ type: "SKIP_WAITING" });
        } else {
          window.dispatchEvent(
            new CustomEvent("sw-update-available", {
              detail: { version: manifest.version },
            }),
          );
        }
}

async function handleUpdateApproved(newVersion) {
  await registerServiceWorker(newVersion);
  navigator.serviceWorker.addEventListener("controllerchange", () => {
    window.location.reload();
  });
}

// Manually trigger skipWaiting to activate a waiting SW immediately
navigator.serviceWorker.ready.then((registration) => {
  if (registration.waiting) {
    registration.waiting.postMessage({ type: "SKIP_WAITING" });
  }
});

The generated sw-injector.js exports a skipWaiting() helper that sends this message. Call it after the user approves an update:

import { skipWaiting } from "./swoff/sw-injector.js";

async function onUpdateApproved() {
  await registerServiceWorker(newVersion);
  skipWaiting();
}

Handling Updates

window.addEventListener("sw-update-available", (e) => {
  const { version } = e.detail;
  const currentVersion = localStorage.getItem("swRegisteredVersion");
  showUpdatePrompt(currentVersion, version);
});

async function handleUpdateApproved(newVersion) {
  await registerServiceWorker(newVersion);
  navigator.serviceWorker.addEventListener("controllerchange", () => {
    window.location.reload();
  });
}

Tracking Progress

// In your app
navigator.serviceWorker.addEventListener("message", (event) => {
  if (event.data.type === "SW_PROGRESS") {
    const { percent, downloaded, total } = event.data;
    updateProgressBar(percent, downloaded, total);
  }
});

The SW reports progress via postMessage during the install event. See SW Template for the SW-side implementation.

The payload includes percent (0-100 integer), downloaded, and total — use whichever suits your UI best.

Learn how to set the X-SW-Cache-Strategy header on client requests: API Integration.

Deferred Registration

Edit the internal shouldRegister() function in sw-injector.js:

function shouldRegister() {
  const welcomeSeen = localStorage.getItem("welcome-seen") === "true";
  const securitySetup = localStorage.getItem("security-setup") === "true";
  return welcomeSeen && securitySetup;
}

Error Handling

Registration can fail for several reasons.

CauseFix
Not HTTPSServe over HTTPS or localhost
Wrong pathVerify SW URL matches the generated file
Wrong MIME typeEnsure .js files serve as text/javascript
Scope mismatchSW must be at root or explicitly set scope

Handle failures gracefully in initServiceWorker():

async function initServiceWorker() {
  if (!("serviceWorker" in navigator)) {
    console.warn("Service Workers not supported");
    return;
  }

  if (!shouldRegister()) return;

  try {
    const manifest = await checkForUpdate();
    const swUrl = `/sw-v${manifest.version}.js`;
    const registration = await navigator.serviceWorker.register(swUrl);
    // registration success logic...
  } catch (error) {
    console.error("SW registration failed:", error);
    window.swError = true;
    window.dispatchEvent(new CustomEvent("sw-error"));
    // App continues without SW — data won't be cached offline
  }
}

Registration Failure Recovery

The app should keep working without offline caching:

async function initializeApp() {
  renderApp();
  await initServiceWorker();
  if (window.swError) {
    showOfflineUnavailableBanner();
  }
}

Update Failure

A new version of the SW fails to install or activate:

async function handleUpdateFailure() {
  const registration = await navigator.serviceWorker.getRegistration();

  if (registration) {
    await registration.unregister();
    const manifest = await checkForUpdate();
    await navigator.serviceWorker.register(`/sw-v${manifest.version}.js`);
  }
}

Scope Errors

The SW controls fewer pages than expected:

// In client, pass scope during registration:
await navigator.serviceWorker.register("/sw.js", { scope: "/" });

Scope is determined by SW file location — /sw.js controls /, /js/sw.js controls /js/*.

Global Error Handler

Catch unhandled errors at the window level:

window.addEventListener("error", (event) => {
  console.error("Unhandled error:", event.error);
});

window.addEventListener("unhandledrejection", (event) => {
  console.error("Unhandled promise rejection:", event.reason);
});

TypeScript Declarations

Create swoff/swoff.d.ts:

interface BeforeInstallPromptEvent extends Event {
  prompt(): Promise<void>;
  userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
}

declare global {
  interface Window {
    deferredInstallPrompt: BeforeInstallPromptEvent | null;
    latestSWVersion?: string;
    currentSWVersion?: string;
    swRegisteredVersion?: string;
    swAvailableVersion?: string;
    swUpdateRequired?: boolean;
    swMinSupportedVersion?: string;
    swReady?: boolean;
    swError?: boolean;
  }
}
export {};

Window Properties

PropertyTypePurpose
window.latestSWVersionstringFrom version.json (server version)
window.currentSWVersionstringVersion of the active/registered SW
window.swRegisteredVersionstringSame as localStorage value
window.swAvailableVersionstringPending update version
window.swUpdateRequiredbooleanWhether update is mandatory
window.swMinSupportedVersionstringMinimum supported version from version.json
window.swReadybooleanSW is active
window.swErrorbooleanSW registration failed

Next Steps

On this page