Skip to content
portfolioblogoffline-first-sync-field-notes
cd ../blog

10 Mar 2026 · 2 min read

Field notes on offline-first sync: what I learned before writing any code

ArchitectureSyncMobile

I recently spent time researching architectures for an offline-first app: a local database on the device, a remote source of truth, and changes flowing both ways. The libraries in this space — PowerSync, ElectricSQL, Replicache/Zero, RxDB and others — all make different trade-offs, but reading through their designs surfaced the same handful of hard problems. These are my notes.

The whiteboard version lies

Every sync diagram looks like this:

local DB  ⇄  sync engine  ⇄  remote DB

The arrows hide the three questions that actually define your architecture:

  1. What happens when both sides change the same row? (conflicts)
  2. How do you identify rows created offline? (identity)
  3. How does a delete on one side reach the other? (tombstones)

If a library's docs don't answer all three explicitly, keep reading until you find out — you will meet these in production either way.

Conflicts: pick a strategy you can explain

The common options, roughly in order of complexity:

  • Last-write-wins (LWW). Simple, predictable, and silently drops data. Fine for "preferences", dangerous for anything a user typed.
  • Field-level merge. Two devices editing different columns of the same row both win. Most engines that track changes per-column give you this nearly for free.
  • Application-level resolution. The server replays intentions ("add 1 to quantity") rather than states ("quantity = 5"). Most correct, most work.
  • CRDTs. Mathematically guaranteed convergence, at the cost of data-model constraints and storage overhead. Worth it for collaborative text; usually overkill for business CRUD.

The honest insight: for most apps, LWW per field plus a server-side audit log covers 95% of cases, and the audit log saves you when the other 5% file a support ticket.

Identity: UUIDs at the edge

Rows created offline can't wait for the server to assign an ID — anything referencing them would need rewriting after sync. Generate UUIDs (v4, or v7 if you want index-friendly ordering) on the device and make them the primary key everywhere. Sequential integer IDs and offline creation simply don't mix.

Deletes are not absence

If a device deletes a row while offline, the server can't distinguish "deleted" from "never synced" unless you keep a record. That's the tombstone: a soft-delete marker that propagates like any other change and gets garbage-collected after every client has seen it. Skipping tombstones is the classic source of "deleted items keep coming back" bugs.

Where the libraries differ

What I took from comparing the current crop:

  • Postgres-centric engines (PowerSync, ElectricSQL) shine when the remote DB already exists and you want partial replicas of it on-device, declared via sync rules.
  • Replicache-style engines push conflict resolution into your own mutators — more control, more responsibility.
  • Local-first databases (RxDB and similar) start from the device side and treat the backend as replication target.

None of them removes the three hard problems. They just choose default answers — and the right library is the one whose defaults match what you'd have chosen anyway.

The takeaway

Offline-first is less a feature than a data-architecture commitment: client-generated IDs, change tracking, tombstones and an explicit conflict policy, decided up front. Retrofitting any of these onto a live schema is painful. Deciding them before the first migration is cheap.

// thanks for reading

Questions, corrections or war stories of your own? Email me — I read everything.