Daeseon Yoo
Back to project
·Tech retro·2 min

Two-way sync without clobbering — per-item last-write-wins with tombstones

The desktop pushes its state to the cloud and the web reads it. The first version was a full-replace push that silently wiped web edits and only flowed one way. The fix: per-item last-write-wins with timestamps and tombstones, plus shrinking the two-way surface so most data stays one-way.

The bug

The founder checked a few checklist items on the web. Nothing showed up on the desktop, and the cloud table was empty even though tickets and widgets had synced. Two bugs at once:

  1. The desktop sync pushed checklist state as delete where service_id = X then insert (the local checked set) — a full replace. Every 20-second push wiped any web check that wasn't already in the desktop's local set.
  2. Sync was one-way (desktop → cloud). The desktop never pulled, so a web edit could never reach it.

And a third trap lurking underneath: a naive periodic two-way sync would resurrect deletes — uncheck something on one side, and the next merge brings it back, because "uncheck" was modeled as deleting the row.

The fix: per-item LWW + tombstones + a small surface

Widget content got the same treatment: a timestamped store on the desktop tracks each key's last-edit time, and the sync reconciles by timestamp instead of overwriting.

What it is honest about

This is whole-value last-write-wins. For one person across two devices it's correct. For two people editing the same widget at the same time, the later save wins and the other's change is lost — a real limitation, documented in the code, deferred until it actually bites. The coordination layer (locks + presence, built next) is what prevents most of that in the first place.

Verified

A unit test drives the whole syncStartup orchestration through a mock Supabase: cloud-newer items pull, local-newer items push, and a pulled item is never re-pushed (no echo loop). The LWW primitives — timestamp tracking, apply-remote, tombstones — are tested directly. Desktop test suite stayed green throughout, because the hard rule never changed: the desktop is local-first and must work fully offline; the cloud is an additive, opt-in copy.

Lesson

"Sync as bulk replace" silently clobbers the other side. Bidirectional sync has to be per-item LWW with tombstones, and you keep it safe by making everything that can be one-way, one-way — not by syncing more carefully.