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
- Write each edit and its outbox entry in one local transaction; flush the outbox in order when online.
- Give every queued operation a client-generated id and dedupe on the server so reconnect replays apply once.
- Send the base version or ETag with If-Match; on 412, refetch, rebase or merge, and surface real conflicts to the user.
- Check the idempotency record before the precondition, or a replay of an applied edit looks like a conflict.
- Limit last-write-wins to fields where losing a concurrent edit is acceptable, and never trust device clocks for ordering.
- 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?RELATED SYMPTOMS
SOURCES
Guide reviewed