Day 12: Sync Without Servers

sync operations offline merge-engine conflicts

Most apps can't sync without a server. We just built sync mechanics that work with USB drives, email attachments, or carrier pigeons. The network is just a transport detail—the real problem was already solved.

The Sync Paradox

Everyone builds sync backwards. They pick a sync provider first—Firebase, Supabase, custom WebSocket servers—then twist their data model to fit.

We did the opposite. We asked: What does sync actually need?

  • A way to describe what changed (operations)
  • A way to package those changes (bundles)
  • A way to merge them safely (merge engine)
  • A way to handle conflicts (explicit resolution)

Notice what's missing? The network. Sync is a data problem, not a network problem.

Sync Bundles: Operations as Cargo

A Sync Bundle is just a JSON file containing operations:

{
  "bundleVersion": 1,
  "createdAt": 1702934400000,
  "workspaceId": "ws_abc123",
  "workspaceName": "My Finances",
  "baseOperationId": null,
  "operations": [
    { "opType": "transaction.create", ... },
    { "opType": "transfer.create", ... },
    { "opType": "reconciliation.toggle", ... }
  ]
}

Key design decisions:

  • Operations only — No full entity snapshots. Changes are small.
  • Chronologically ordered — Replay in sequence produces consistent state.
  • Human-readable JSON — You can open it in a text editor.
  • Incremental support — "Since last sync" bundles are tiny.

The Deterministic Merge Engine

When you import a bundle, the merge engine does three things:

1. Detect Duplicates

Operations with the same ID are skipped. Importing twice is safe.

2. Detect Conflicts

Same entity modified differently? Update vs delete? We catch it.

3. Accept Clean

New operations that don't conflict are accepted immediately.

The merge is deterministic. Same inputs, same outputs. Always.

Conflicts Are Features, Not Bugs

Most sync systems hide conflicts. They use "last write wins" or "most recent timestamp" and hope nobody notices when data silently disappears.

We show you everything:

Conflict TypeWhat Happened
update_updateSame transaction edited differently on both devices
update_deleteYou edited it, they deleted it
delete_updateYou deleted it, they edited it
rule_manualRule changed it on one device, manual change on another

For each conflict, you choose:

  • Keep Local — Your version wins
  • Accept Incoming — Their version wins
  • Create Duplicate — Keep both versions
  • Merge Manually — Review and fix yourself

No silent data loss. Ever.

How It Works In Practice

// Device A: Working offline
Created: Transaction "Coffee" -$4.50
Updated: Transaction "Groceries" → "Whole Foods"

// Device B: Working offline
Created: Transaction "Gas" -$45.00
Deleted: Transaction "Groceries"

// Later: Device A exports bundle
Export → sync-bundle-2024-12-19.json

// Device B imports bundle
Import ← sync-bundle-2024-12-19.json

✓ "Coffee" accepted (new operation)
⚠ "Groceries" conflict (update vs delete)
  → User chooses: Keep deletion

// Result: Both devices now match

Why No Networking?

This sprint deliberately excluded all networking. Why?

  • Separation of concerns — Sync logic and transport logic are independent
  • Offline-first verification — We can prove it works without any server
  • Multiple transport options — USB, email, cloud storage, WebRTC—all work
  • No lock-in — Your data, your choice of how to move it

The Test Suite

We added 13 new tests for sync mechanics:

// Sync Bundle Tests
✓ Full bundle exports all operations
✓ Valid bundle passes validation
✓ Invalid bundle fails validation
✓ Bundle without operations fails validation
✓ Future version bundle fails validation
✓ Importing same bundle twice is idempotent
✓ Merge detects update conflicts within concurrency window
✓ Incremental bundle exports operations since base
✓ Sync state is tracked per peer
✓ Conflict resolution generates appropriate options
✓ Bundle exports to valid JSON blob
✓ Operations are chronologically ordered
✓ Ledger invariants hold after merge

Total: 58 tests pass (45 previous + 13 new)

What We Built

sync-bundle.ts

Bundle creation, validation, export. Tracks sync state per peer.

merge-engine.ts

Deterministic merge, conflict detection, resolution options.

operation-replay.ts

Replays operations through LedgerService. Idempotent.

/sync UI

Export bundles, import bundles, resolve conflicts.

What Comes Next

With sync mechanics complete, we can now add optional transports:

  • WebRTC — Peer-to-peer sync without servers
  • Cloud relay — Optional server for convenience
  • QR codes — Scan to sync between phones

But all of those are optional. The bundle-and-merge system works without any of them.

Sync used to mean "connect to our servers." Now it means "exchange these files however you want."

Your data. Your rules. Your transport.

— The Accelerate Finance Team