Offline Mutations & Sync
Queue writes offline, replay online, background-sync, and framework adapters.
If you're coming from RxDB for offline sync: Swoff doesn't replicate a client-side database. Instead, it queues POST/PUT/PATCH/DELETE requests in IndexedDB when offline and replays them when connectivity returns. No schema, no replication protocol, no WebSocket sync engine. Each queued mutation is just a stored HTTP request. See the Library Comparison — offline write queue row.
Preconditions
- Swoff initialized with data fetching enabled
Bearer and custom auth limitation: Background sync runs from the service worker with no DOM access — bearer tokens can't be refreshed and custom headers can't be injected. Only cookie auth supports background sync. Bearer and custom auth work with the mutation queue (online replay only — see the table below).
| Auth type | Mutation queue | Background sync |
|---|---|---|
| cookie | ✅ | ✅ |
| bearer | ✅ | ❌ |
| custom | ✅ | ❌ |
Status
Disabled by default. The mutation queue must be explicitly enabled — it's not part of the default generated config.
Enable
# Step 1: queue mutations when offline
npx @swoff/cli add mutation-queue# Step 2 (optional): enable Background Sync API for sync after tab close
# Requires cookie auth
npx @swoff/cli add background-syncOr set config manually:
{
"features": {
"mutationQueue": {
"enabled": true,
"backgroundSync": true,
"batchSize": 1,
"batchDelayMs": 0,
"retry": {
"maxRetries": 5,
"backoffMs": 1000,
"maxBackoffMs": 30000,
"jitterMs": 250
}
}
}
}Generated files
| File | What it does | Import in your code? |
|---|---|---|
swoff/mutation/queue.ts | queueMutation(), processMutationQueue(), clearQueue(), flushMutations(), getPendingCount(), getQueueItems(), getQueuePosition() | Yes |
swoff/mutation/state.ts | Mutation state tracking (per-mutation lifecycle) | Yes, for UI |
swoff/mutation/sync.ts | syncWhenPossible(), retrySync() | Yes, for background sync |
swoff-api.bundle.js | Same mutation API on window.swoff for no-bundler projects | No (auto-initializes) |
Usage
import { fetchWithCache } from "./swoff/fetch/core";
import {
getPendingCount,
getQueueItems,
clearQueue,
flushMutations,
} from "./swoff/mutation/queue";
// Queue a write — works offline, replays when online
const { response, queued } = await fetchWithCache("/api/notes", {
method: "POST",
body: JSON.stringify({ title: "New Note" }),
mutation: true, // 👈 queues offline, replays on reconnect
});
// queued === true means it was stored offline for later replay
// On successful replay, auto-invalidates related tags
// Show sync status
const count = await getPendingCount();
if (count > 0) {
console.log(`${count} mutations waiting to sync`);
}
// Inspect queue
const items = await getQueueItems();
// items = [{ id, method, url, body, retryCount, status, ... }]
// Clear all queued mutations (e.g., on logout — note: clearAuth() also calls clearQueue automatically)
await clearQueue();
// Force sync all pending mutations now (e.g., user clicks "Sync Now")
await flushMutations();From any HTTP client (header-based)
No swoff import needed — the SW intercepts every fetch and queues it on network failure when it detects the X-SW-Type: mutation header:
fetch("/api/notes", {
method: "POST",
body: JSON.stringify({ title: "New Note" }),
headers: { "X-SW-Type": "mutation" },
});<!-- HTMX — header triggers SW offline queue -->
<button
hx-post="/api/notes"
hx-headers='{"X-SW-Type": "mutation"}'
hx-vals='{"title": "New Note"}'
>
Create Note
</button>Works with any HTTP client: native fetch, HTMX, jQuery, curl, or server-rendered forms. The SW auto-invalidates the request's URL tags on successful replay, same as fetchWithCache({ mutation: true }).
Background sync
import { syncWhenPossible } from "./swoff/mutation/sync";
// Queue with background sync registration
await syncWhenPossible({
method: "POST",
url: "/api/notes",
body: { title: "New Note" },
tags: ["notes"],
});
// In unsupported browsers, falls back to `online` event listenerNo-bundler usage
Include swoff-api.bundle.js after client-injector.bundle.js. You have three ways to queue mutations:
Swoff fetch swoff.fetchWithCache({ mutation: true }) (recommended)
Auto-queues when offline, replays when connectivity returns, auto-invalidates related tags on success:
<script src="/swoff/swoff-api.bundle.js"></script>
<script>
const { response, queued } = await swoff.fetchWithCache("/api/notes", {
method: "POST",
body: JSON.stringify({ title: "New Note" }),
mutation: true,
});
// queued === true means it was stored offline for later replay
</script>Native fetch with X-SW-Type: mutation header
Works from any server-rendered page, HTMX, or external HTTP client without any swoff import:
<!-- HTMX — header triggers SW offline queue -->
<button
hx-post="/api/notes"
hx-headers='{"X-SW-Type": "mutation"}'
hx-vals='{"title": "New Note"}'
hx-trigger="click"
>
Create Note
</button>// Plain fetch — SW intercepts and queues when offline
fetch("/api/notes", {
method: "POST",
body: JSON.stringify({ title: "New Note" }),
headers: { "X-SW-Type": "mutation" },
});The SW detects the header and queues the request in IndexedDB on network failure. No swoff import needed.
Background Sync API swoff.syncWhenPossible()
Registers the mutation for the browser's SyncManager. The browser replays it in the background even after the user closes the tab. Requires cookie auth and backgroundSync: true in config.
<script>
await swoff.syncWhenPossible({
method: "POST",
url: "/api/notes",
body: { title: "New Note" },
tags: ["notes"],
});
</script>syncWhenPossible vs flushMutations:
| API | What it does | When the mutation replays |
|---|---|---|
flushMutations() | Force-replay all queued mutations immediately | Right now, synchronously |
syncWhenPossible() | Register for browser Background Sync API | When the browser decides (usually on connectivity), survives tab close |
In browsers without SyncManager support, syncWhenPossible() falls back to an online event listener — the mutation replays the next time the browser detects connectivity.
Queue inspection and management
<script>
// Inspect queue
const count = await swoff.getPendingCount();
const items = await swoff.getQueueItems();
// items = [{ id, method, url, body, retryCount, status, ... }]
// Clear all queued mutations (also done automatically by clearAuth())
await swoff.clearQueue();
</script>Framework adapters
This section has four adapters:
useSwoffQueue
Exposes mutation queue state: { pending, items, isProcessing, retryAll }.
Events listened to:
| Event | Detail shape | Purpose |
|---|---|---|
mutation-queue-changed | — | Queue contents changed (mutation added or processed) |
If your framework already has these adapters (useSwoffMutation, useSwoffQueue, useSwoffMutationState, useSwoffSync — React, Vue, Svelte), the CLI generates them into swoff/adapters/. To create adapters for any other framework, see the Blueprint for the event reference and adapter patterns.
Config
{
"features": {
"mutationQueue": {
"enabled": true,
"batchSize": 1,
"batchDelayMs": 0,
"backgroundSync": false,
"retry": {
"maxRetries": 5,
"backoffMs": 1000,
"maxBackoffMs": 30000,
"jitterMs": 250
}
}
}
}batchSize— emit progress event every N mutationsbatchDelayMs— delay between individual mutation replays (rate limiting)retry.maxRetries— max replay attempts before droppingretry.backoffMs— initial backoff delayretry.maxBackoffMs— max backoff capretry.jitterMs— random jitter added to backoff to prevent thundering herd
Related
- Library Comparison — offline write queue feature row vs Workbox / Serwist