SwoffSwoff

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 typeMutation queueBackground 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-sync

Or set config manually:

{
  "features": {
    "mutationQueue": {
      "enabled": true,
      "backgroundSync": true,
      "batchSize": 1,
      "batchDelayMs": 0,
      "retry": {
        "maxRetries": 5,
        "backoffMs": 1000,
        "maxBackoffMs": 30000,
        "jitterMs": 250
      }
    }
  }
}

Generated files

FileWhat it doesImport in your code?
swoff/mutation/queue.tsqueueMutation(), processMutationQueue(), clearQueue(), flushMutations(), getPendingCount(), getQueueItems(), getQueuePosition()Yes
swoff/mutation/state.tsMutation state tracking (per-mutation lifecycle)Yes, for UI
swoff/mutation/sync.tssyncWhenPossible(), retrySync()Yes, for background sync
swoff-api.bundle.jsSame mutation API on window.swoff for no-bundler projectsNo (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 listener

No-bundler usage

Include swoff-api.bundle.js after client-injector.bundle.js. You have three ways to queue mutations:

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:

APIWhat it doesWhen the mutation replays
flushMutations()Force-replay all queued mutations immediatelyRight now, synchronously
syncWhenPossible()Register for browser Background Sync APIWhen 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:

EventDetail shapePurpose
mutation-queue-changedQueue 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 mutations
  • batchDelayMs — delay between individual mutation replays (rate limiting)
  • retry.maxRetries — max replay attempts before dropping
  • retry.backoffMs — initial backoff delay
  • retry.maxBackoffMs — max backoff cap
  • retry.jitterMs — random jitter added to backoff to prevent thundering herd

On this page