← Back to the library
MobileDeep dive · 10 min

Offline edits conflict or vanish on reconnect

“Users edit while offline; after reconnecting, their changes overwrite someone else's, get overwritten, or are applied twice.”

THE PRINCIPLE

Offline sync is a distributed system with very long partitions. Without base versions, the server cannot tell an edit from an overwrite.

FIRST MOVES

  1. Write each edit and its outbox entry in one local transaction; flush the outbox in order when online.
  2. Give every queued operation a client-generated id and dedupe on the server so reconnect replays apply once.
  3. Send the base version or ETag with If-Match; on 412, refetch, rebase or merge, and surface real conflicts to the user.
  4. Check the idempotency record before the precondition, or a replay of an applied edit looks like a conflict.
  5. Limit last-write-wins to fields where losing a concurrent edit is acceptable, and never trust device clocks for ordering.
  6. Use a CRDT (Automerge, Yjs) for collaborative text and lists; keep invariants such as balances and stock under server authority.

TOOLS: THEN → NOW

Whole-record PUT on reconnectPer-operation outbox with field-level patches and base versions
Hand-rolled sync over SQLiteSync engines and local-first stores such as Automerge; custom sync remains reasonable for simple models

PATTERN SNAPSHOT

async function flush(op: OutboxOp) {
  const res = await fetch(`/api/notes/${op.entityId}`, {
    method: "PATCH",
    headers: {
      "Idempotency-Key": op.id,        // replay-safe across reconnects
      "If-Match": op.baseEtag,         // version the edit was based on
      "Content-Type": "application/merge-patch+json",
    },
    body: JSON.stringify(op.patch),
  });
  if (res.status === 412) return markConflict(op); // refetch, rebase, or ask
  if (res.ok) return ack(op, res.headers.get("ETag"));
  throw new Error(`retry later: ${res.status}`); // keep op queued
}

CLOSE THE AI. EXPLAIN THIS.

Why can an offline edit that the server already applied come back as a 412 conflict on replay, and how do you prevent it?

SOURCES

Guide reviewed