SwoffSwoff

Auth-Aware Caching

Auth headers, 401 detection, token refresh, protected routes, cross-tab sync.

Swoff's auth layer injects headers automatically, refreshes tokens on 401, and clearAuth() purges memory, IndexedDB, caches, and cross-tab state in one call.

Preconditions

  • A backend with login/logout endpoints and a session or token mechanism
  • swoff init already run

Status

Disabled by default. Auth must be explicitly enabled — it's not part of the default generated config.

Enable

npx @swoff/cli add auth

Defaults to cookie auth (auth.type: "cookie"). To use bearer tokens:

# Edit swoff.config.json first, then:
npx @swoff/cli add auth

Or set features.auth.enabled: true and features.auth.type in swoff.config.json then regenerate.

Generated files

FileWhat it doesImport in your code?
swoff/auth/adapter.tsMaps Swoff auth to your provider. Defines AuthData + exports adapter with getAuth, getHeaders, refresh, fetchUser.Yes — import adapter for type reference
swoff/auth/store.tssetAuth(), getAuth(), clearAuth(), ensureValidAuth(), isAuthValid(), withAuthHeaders(), isAuthUrl(), AUTH_WITH_CREDENTIALSYes — main auth API
swoff/auth/state.tsgetAuthState() — 4-state matrix (online/offline × authed/unauthed)Yes
swoff/auth/check.tsisAuthFailureResponse() — customize what both the SW and client treat as auth failureYes — edit this file
swoff-api.bundle.jsNo-bundler equivalent — swoff.configure({auth: {...}}), swoff.setAuth(), swoff.clearAuth(), swoff.getAuthState(), swoff.clearMemoryAuth() on window.swoffNo (auto-initializes)

Usage

Auth is framework-agnostic — the generated modules work the same in React, Laravel Blade, HTMX, or any setup.

Bundler projects

import { fetchWithCache } from "./swoff/fetch/core";
import {
  setAuth,
  clearAuth,
  getAuth,
  isAuthValid,
  ensureValidAuth,
} from "./swoff/auth/store";
import { getAuthState } from "./swoff/auth/state";

// After login — pass the user data from your backend
const user = await fetchWithCache("/api/me", { auth: true }).then((r) =>
  r.json(),
);
await setAuth(user);

// Authenticated requests — auth headers injected automatically
const { response } = await fetchWithCache("/api/protected", { auth: true });

// Check auth state anywhere
const { authenticated, auth, online } = await getAuthState();
// States: online+authed, online+unauthed, offline+authed, offline+unauthed

// On logout — clears memory, IDB, runtime caches, mutation queue, and broadcasts to other tabs
await clearAuth();

No-bundler projects

Configure auth at runtime via swoff.configure(), then use the API on window.swoff:

<script src="/swoff/client-injector.bundle.js"></script>
<script src="/swoff/swoff-api.bundle.js"></script>
<script>
  swoff.configure({
    auth: {
      type: "cookie",
      getHeaders: () => ({}),
      refresh: async () => null,
      fetchUser: async () => null
    }
  });

  const user = await swoff.fetchWithCache("/api/me", { auth: true });
  await swoff.setAuth(user.data);

  // Check auth state — 4-state matrix
  const { authenticated, online } = await swoff.getAuthState();

  // Memory-only auth clearing (does not touch IndexedDB)
  await swoff.clearMemoryAuth();

  // Full clear — memory, IDB, runtime caches, mutation queue, cross-tab
  await swoff.clearAuth();
</script>

For cookie auth (e.g., Laravel Sanctum, Django session), the browser sends the cookie automatically — no token management needed. For bearer auth, tokens are stored in memory only and never exposed to the Service Worker scope.

Bearer-only — token refresh is automatic when the SW detects a 401 on { auth: true } requests:

// The SW intercepts the 401, calls adapter.refresh() silently,
// retries the original request with the new token.
// Your code just sees the successful response.

Customize

swoff/auth/adapter.ts (bundler) — you must edit this file

AuthData

The single source of truth for auth shapes across the client, store, and state modules:

export interface AuthData {
  token?: string; // Bearer token (cookie auth omits this)
  user?: unknown; // Replace `unknown` with your backend's user type
  expiresAt?: number; // Unix ms — used by isAuthValid() / ensureValidAuth()
}

Edit the user field type to match your backend's user object.

Adapter methods

The exported adapter object has four methods:

export const adapter: {
  type: "cookie" | "bearer";
  getAuth: () => Promise<AuthData | null>;
  getHeaders: (auth: AuthData | null) => Record<string, string>;
  refresh: (auth: AuthData) => Promise<AuthData | null>;
  fetchUser: () => Promise<AuthData | null>;
};
MethodPurposeCookie behaviorBearer behavior
getAuth()Return current auth from your provider. Used by getAuth() in store.ts as first lookup.Return null — browser sends cookie automatically.Return null — token lives in memory, not in adapter.
getHeaders(auth)Generate auth headers for fetchWithCache requests.Return {} — browser sends cookie automatically.Return { Authorization: "Bearer <token>" } if token present.
refresh(auth)Refresh the session when SW detects a 401. Called by ensureValidAuth() / SW on auth-failure.Return null — server manages session lifetime.POST to /api/refresh with existing token, return new { token, expiresAt }.
fetchUser()Fetch current user from server. Used by getAuth() in store.ts as last-resort fallback.GET /api/me with credentials: "include".GET /api/me with Bearer header.

The type field controls AUTH_WITH_CREDENTIALS in store.ts (true for cookie, false for bearer).

swoff/auth/check.ts (bundler) — customize auth failure detection

Default: response.status === 401. Override if your backend uses a different signal:

export async function isAuthFailureResponse(response) {
  // Custom header
  return response.headers.get("X-Auth-Status") === "expired";
  // Or JSON body (must clone first)
  const data = await response.clone().json();
  return data.error === "unauthorized";
}

No-bundler: runtime auth adapter

The no-bundler API bundle (swoff-api.bundle.js) accepts an auth adapter via swoff.configure(). The adapter shape is the same as in auth/adapter.ts — pass getHeaders, refresh, fetchUser, and type at runtime instead of editing a generated file.

<script>
  swoff.configure({
    auth: {
      type: "bearer",
      getHeaders: (auth) =>
        auth?.token ? { Authorization: `Bearer ${auth.token}` } : {},
      refresh: async (auth) => {
        const res = await fetch("/api/refresh", { method: "POST" });
        return res.ok ? res.json() : null;
      },
      fetchUser: async () => {
        const res = await fetch("/api/me");
        return res.ok ? res.json() : null;
      },
    },
  });
</script>

For server-rendered pages, call swoff.configure() after the bundles load, before any auth-dependent fetchWithCache calls.

Framework adapters

This section has one adapter:

useSwoffAuth

Exposes auth state: { authenticated, auth, online, setAuth, clearAuth }.

Events listened to:

EventDetail shapePurpose
sw-auth-state-change{ type: "login" | "logout" | "refresh" | "clear" }Auth state changed

If your framework already has a useSwoffAuth adapter (React, Vue, Svelte), the CLI generates it into swoff/adapters/. To create adapters for any other framework, see the Blueprint for the event reference and adapter patterns.

Config

{
  "features": {
    "auth": {
      "enabled": true,
      "type": "cookie",
      "routePaths": ["/api/login", "/api/logout", "/api/refresh", "/api/me"]
    }
  }
}
  • auth.type"cookie" (httpOnly session cookie), "bearer" (token), or "custom"
  • auth.routePaths — URL paths that bypass SW cache (auth endpoints must not be cached)

Auth type vs feature compatibility

Auth typePWAMutation queueBackground syncServer pushGraphQL
cookie
bearer
custom

Background sync and server push require cookie auth because they run in the SW scope (no DOM, no token refresh possible).

On this page