Swoff Swoff

Offline-First Architecture

Build apps with offline-capable app shell and smart data handling.

Principles

Build web apps where the app shell works offline and data is handled intelligently:

  1. Offline App Shell — All routes and static assets cached via Service Worker
  2. Smart Data — Read requests cached, mutations queued when offline
  3. Client Runtime — Data lives on the client (IndexedDB), syncs with your backend when online

Client Runtime

Swoff makes the browser a capable client runtime. The app shell is fully cached, read responses are cached for offline access, and mutations are queued when offline — ready to sync when connection returns.

App Shell & Rendering Modes

"All routes cached" works differently depending on how your framework renders pages:

ModeHow Shell WorksSW Caching StrategyExamples
SPASingle index.html + JS bundles. All routes served from the same HTML.Cache index.html + assets at install. Serve index.html for all navigation (SPA fallback).React/Vue/Svelte via Vite
SSGPre-built HTML files for each route. Fully static output.Cache all HTML files + assets at install via asset list. CacheFirst for everything.Astro, Next.js SSG, Nuxt SSG, Gatsby
SSRHTML generated per-request. Only static assets are pre-buildable.NetworkFirst for HTML (cache after first visit). CacheFirst for JS/CSS/images.Next.js, Nuxt, SvelteKit, Remix
HybridMix of static and dynamic routes (e.g., ISR).Pre-cache static routes at install. NetworkFirst for dynamic routes after first visit.Next.js ISR, Nuxt Hybrid

Your bundler (Vite, Webpack, Turbopack) generates the output that feeds the SW's asset list (// [[ASSETS_LIST]]). For a SPA that's ['/index.html']; for SSG it's the full set of route HTML files.

With SSR, consider stale-while-revalidate for HTML routes: serve cached HTML immediately (after first visit) while fetching fresh in the background. This gives instant loads without sacrificing freshness.

┌────────────────────────┐
│     Browser Tab         │
│  ┌──────────────────┐ │
│  │  React/Vue App   │ │
│  │  IndexedDB data  │ │
│  └──────────────────┘ │
│  ┌──────────────────┐ │
│  │ Service Worker    │ │
│  │ (cache storage)   │ │
│  └──────────────────┘ │
└────────────────────────┘

Storage Strategy

APICapacityRead CachingMutation Queue
Cache APIUnlimited (depends on free disk)Read responses cached
IndexedDBUnlimited (depends on free disk)Queue mutations, store local data
localStorage~5-10 MBSettings, SW versionSettings, metadata
sessionStorage~5-10 MBSession stateSession state

Limited by disk space and browser quota. See Storage Quota Management for quota handling and iOS Safari Limitations for platform-specific storage limits.

Cache API (Read Response Caching)

When online, fetch and cache read responses; when offline, serve from cache. The Service Worker handles this transparently — see the SW Template for the implementation.

// Core concept: read-through cache
const response = (await caches.match(url)) || (await fetch(url));

Tier 1: IndexedDB (Mutation Queue)

When offline, queue mutations locally. When back online, drain the queue. See IndexedDB Patterns for the implementation.

// Core concept: queue offline mutations
await db.add("mutation-queue", { ...mutation, timestamp: Date.now() });

Data Flow

Online (First Visit):

  1. Browser loads index.html
  2. SW registers and caches app shell
  3. App fetches data, caches read responses
  4. Mutations sent to backend normally

Offline:

  1. SW serves app shell from cache
  2. Read requests served from Cache API
  3. Mutations queued in IndexedDB
  4. When back online: queued mutations synced to backend

Session Management

// On login
sessionStorage.setItem("auth", "true");

// On page load
if (sessionStorage.getItem("auth") === "true") {
  showApp();
} else {
  showLogin();
}

Next Steps

On this page