Queue Offline Mutations

Queue writes explicitly. Do not rely on a background API to remember business data for you.

Mutation Flow

type OutboxItem = {
  id: string;
  url: string;
  method: 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  body: unknown;
  createdAt: string;
  attempts: number;
};

async function createEntry(entry: unknown) {
  const item: OutboxItem = {
    id: crypto.randomUUID(),
    url: '/api/entries',
    method: 'POST',
    body: entry,
    createdAt: new Date().toISOString(),
    attempts: 0,
  };

  await saveToIndexedDb('outbox', item);
  await replayOutbox();
}

Replay Rules

async function sendOutboxItem(item: OutboxItem) {
  const response = await fetch(item.url, {
    method: item.method,
    headers: {
      'content-type': 'application/json',
      'idempotency-key': item.id,
    },
    body: JSON.stringify(item.body),
  });

  if (!response.ok) {
    throw new Error(`Replay failed: ${response.status}`);
  }

  await removeFromIndexedDb('outbox', item.id);
}

Conflict Checklist

  • Use client-generated IDs or idempotency keys.
  • Store server version, etag, timestamp, or revision if conflicts are possible.
  • Show pending, failed, and synced states.
  • Keep failed writes editable or exportable.
  • Retry in foreground whenever the app opens or regains network.
  • Add Background Sync only as an optimization.