Concept · Frontend & Mobile

Mobile Offline-First Sync

01

Why this matters

The user opens your app on the subway. No signal. They write a comment, mark a task done, edit a doc. The app should accept their work, not pop "no internet" errors. When connectivity returns, everything they did syncs to the server. That's offline-first — and it's vastly harder than it sounds.

Two phones edit the same doc while both offline. They reconnect. Whose change wins? What if both add a row with the same primary key? What if one phone deleted what the other modified? These are the hard problems mobile apps face and most get partially wrong.

02

The architecture

  1. Local-first storage. SQLite (Android Room, iOS Core Data, Realm). All reads + writes go to local DB first.
  2. Operation queue. Writes also append to a pending-operations queue. "Comment created", "Task done", "Doc edited at offset 42 inserted X."
  3. Sync engine. When online, drains the queue to the server. Server returns canonical state. Local DB updated.
  4. Conflict resolution. Server's authoritative answer might disagree with local optimistic state. Reconcile.
  5. Pull updates. Server pushes (or device pulls) changes from other clients. Apply to local DB. UI updates reactively.

UI never sees "loading" for normal interactions — it reads from local DB, optimistic. Network is a background concern that eventually reconciles.

G-Counter CRDT (grow-only counter)
class GCounter:
    """Each replica tracks its own contribution; total = sum."""
    def __init__(self, replica_id):
        self.replica = replica_id
        self.counts = {replica_id: 0}

    def inc(self, n=1):
        self.counts[self.replica] += n

    def value(self):
        return sum(self.counts.values())

    def merge(self, other):
        for r, v in other.counts.items():
            self.counts[r] = max(self.counts.get(r, 0), v)
        return self

# Two phones increment offline; reconnect; merge = sum of each replica's
# own count. Associative, commutative, idempotent. No conflict resolution needed.

# PN-Counter = two G-counters (inc + dec). Supports decrement.
03

The three sync paradigms

ParadigmHow sync worksUsed by
Last-write-winsServer timestamp wins. Simple. Lossy.Most CRUD apps, contact sync
Operational Transform (OT)Operations transformed to apply against current stateGoogle Docs, Sheets — text editing
CRDT (Conflict-free Replicated Data Type)Data structures that merge automaticallyFigma, Linear, Notion — modern collab apps
04

Deep dive — why CRDTs won

OT requires every operation to be transformed against every concurrent operation — server-side coordination, complex correctness proofs (Google Docs took years to get right). CRDTs (Conflict-free Replicated Data Types) are data structures that mathematically merge regardless of order or duplication. Two clients can apply operations in any order and converge to the same result.

Concrete CRDTs:

  • G-Counter — grow-only counter. Each client tracks its own delta. Sum = total.
  • OR-Set — observed-remove set. Add and remove operations carry unique tags so concurrent add+remove is unambiguous.
  • RGA / LSEQ — sequence types for collaborative text. Each character has a unique position-ID; concurrent inserts produce deterministic ordering.
  • YATA / Yjs — practical CRDT library used by ~all 2024-era collab apps. Powers Linear's editor, the Notion clones, Tldraw, Liveblocks.

The win: zero server-side conflict resolution code. Client A and Client B both run the same merge logic locally; results converge. Server is just a relay + storage. Same code can run peer-to-peer (some apps do this).

CRDT vs LWW Concurrent Update Mermaid
sequenceDiagram participant A as Phone A participant B as Phone B participant S as Server Note over A,B: Both offline; both edit list A->>A: add "milk" B->>B: add "eggs" Note over A,B: Both reconnect A->>S: sync ops B->>S: sync ops Note over S: LWW: latest write wins → one item lost Note over S: CRDT: both ops merge → list = [milk, eggs] S-->>A: full state [milk, eggs] S-->>B: full state [milk, eggs]
05

Practical patterns

  • Use stable client-side IDs. Generate UUIDs locally; don't wait for server-assigned IDs. Server stores client UUIDs; deterministic across sync.
  • Vector clocks for "since when?" Each client tracks "I last synced at vector V"; server returns ops since V. Avoids re-syncing the world.
  • Idempotent operations. Same op applied twice = no double effect. Survives the inevitable retry.
  • Handle deletions specially. "Tombstone" markers prevent zombies (deleted item recreated by another client's old write).
  • Conflict-resolution UI for genuine human conflicts. Two people edit the same paragraph — show both versions, let user pick. Don't pretend you can auto-resolve everything.
  • Battery + bandwidth. Don't poll; use long-poll or push. Sync when on WiFi if possible. Coalesce batches.
Modern stack

"SQLite local DB. Yjs CRDT for collaborative document state. Background sync via WebSocket when foregrounded; push notifications wake the app for high-priority updates. Vector clock tracks last sync point. Batched commits over WiFi to save battery."

06

Real-world

Linear

CRDT-backed real-time collab

Yjs under the hood. Issues edited offline sync seamlessly. No "save" button — everything always synced.

Apple Notes / iCloud

CloudKit sync

OT-style document sync. Notes work offline; reconcile via iCloud. Per-field merge for partial conflicts.

Google Docs

OT pioneer

2010s gold standard for OT. Decades of investment in correctness. Slowly shifting toward CRDT internally.

PowerSync / ElectricSQL / Replicache

Sync-as-a-service

Productize the offline-sync pattern. Devs describe the schema; library handles sync, CRDT-style merges, conflict resolution.

07

Used in problems

Google Drive sync engine is a textbook offline-first system. Google Docs uses OT for character-level collaboration. WhatsApp messages queue locally when offline; deliver on reconnect. Notification system relies on push protocols + offline buffering.

Next up