Swoff Swoff

Token Storage

Store auth tokens securely using IndexedDB + memory cache.

Token Storage

Prerequisites: None. This module is self-contained and independent of your auth mechanism.

Storage Strategy

StoragePersistsScopeContains
MemoryUntil page refreshTabBearer token (if auth type is "bearer")
IndexedDBIndefinitely (swoff-auth DB)Tab{ user, expiresAt } only — no token

The CLI generates this file when features.auth.enabled is true. For "bearer" auth, the token lives in memory only — page refresh requires re-login. { user, expiresAt } is persisted to IndexedDB for offline display. For "cookie" auth, the browser handles credentials automatically.

The bearer token is never written to IndexedDB. It only lives in memory and is lost on page refresh. This is intentional — IndexedDB has the same origin-level access as any JavaScript on your origin. For high-security apps, prefer http-only cookies over bearer tokens.

Implementation

swoff/auth-store.js
// swoff/auth-store.js — Generated by CLI. Token is memory-only.
// Persists only { user, expiresAt } to IndexedDB for offline display.

const DB_NAME = "swoff-auth";
const STORE_NAME = "auth";

let memoryAuth = null;

function openAuthDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(DB_NAME, 1);
    request.onupgradeneeded = (e) => {
      const db = e.target.result;
      if (!db.objectStoreNames.contains(STORE_NAME)) {
        db.createObjectStore(STORE_NAME, { keyPath: "key" });
      }
    };
    request.onsuccess = (e) => resolve(e.target.result);
    request.onerror = (e) => reject(e.target.error);
  });
}

export async function setAuth(authData) {
  memoryAuth = authData;
  const db = await openAuthDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_NAME, "readwrite");
    const store = tx.objectStore(STORE_NAME);
    // Persist only user + expiresAt — no token
    store.put({ key: "session", value: { user: authData.user, expiresAt: authData.expiresAt } });
    tx.oncomplete = () => resolve();
    tx.onerror = () => reject(tx.error);
  });
}

export async function getAuth() {
  if (memoryAuth) return memoryAuth;
  const db = await openAuthDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_NAME, "readonly");
    const store = tx.objectStore(STORE_NAME);
    const request = store.get("session");
    request.onsuccess = () => {
      const persisted = request.result?.value || null;
      if (persisted) memoryAuth = { ...persisted, token: undefined };
      resolve(memoryAuth || null);
    };
    request.onerror = () => reject(request.error);
  });
}

export async function clearAuth() {
  memoryAuth = null;
  const db = await openAuthDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_NAME, "readwrite");
    const store = tx.objectStore(STORE_NAME);
    store.delete("session");
    tx.oncomplete = () => resolve();
    tx.onerror = () => reject(tx.error);
  });
}

export function isAuthValid(auth) {
  if (!auth) return false;
  if (!auth.expiresAt) return true;
  return Date.now() < auth.expiresAt;
}

Your Login / Logout

These are your functions — Swoff provides the storage infrastructure:

// In your app — you provide login/logout, Swoff provides storage
import { setAuth, clearAuth } from "./swoff/auth-store.js";

async function login(email, password) {
  // Your auth mechanism: JWT, session, OAuth, whatever
  const response = await fetch("/api/login", {
    method: "POST",
    body: JSON.stringify({ email, password }),
  });
  const data = await response.json();

  // Store in Swoff's auth infrastructure
  await setAuth({
    token: data.token, // Your token (or null if http-only cookie)
    user: data.user, // Current user object
    expiresAt: data.expiresAt, // Expiry timestamp (ms), or omit
  });

  window.dispatchEvent(
    new CustomEvent("sw-auth-state-change", {
      detail: { authenticated: true },
    }),
  );
}

async function logout() {
  await fetch("/api/logout", { method: "POST" }).catch(() => {});
  await clearAuth();
  window.dispatchEvent(
    new CustomEvent("sw-auth-state-change", {
      detail: { authenticated: false },
    }),
  );
}

What authData Should Contain

The shape depends on your backend:

FieldRequired?Purpose
tokenCookie: no / Bearer: yesThe token string to attach to requests
userRecommendedCurrent user object for offline display
expiresAtOptionalTimestamp (ms) for offline expiry checking

Next Steps

On this page