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:
- Offline App Shell — All routes and static assets cached via Service Worker
- Smart Data — Read requests cached, mutations queued when offline
- 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:
| Mode | How Shell Works | SW Caching Strategy | Examples |
|---|---|---|---|
| SPA | Single 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 |
| SSG | Pre-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 |
| SSR | HTML 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 |
| Hybrid | Mix 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
| API | Capacity | Read Caching | Mutation Queue |
|---|---|---|---|
| Cache API | Unlimited (depends on free disk) | Read responses cached | — |
| IndexedDB | Unlimited (depends on free disk) | — | Queue mutations, store local data |
| localStorage | ~5-10 MB | Settings, SW version | Settings, metadata |
| sessionStorage | ~5-10 MB | Session state | Session 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):
- Browser loads
index.html - SW registers and caches app shell
- App fetches data, caches read responses
- Mutations sent to backend normally
Offline:
- SW serves app shell from cache
- Read requests served from Cache API
- Mutations queued in IndexedDB
- 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
- Versioned SW System — preventing silent updates
- SW Template — the foundation
Swoff