Overview

UniSave uses delta sync to keep clients in sync with the server. Instead of fetching all data on every sync, clients request only what changed since their last sync using an opaque cursor.

Cursor Design

PropertyDetail
FormatOpaque string — clients must not parse it
Internal encoding(updated_at, client_id) composite key
OrderingAscending by updated_at, then by client_id for ties
StabilityDeterministic — same cursor always returns same page boundary
The cursor encodes the position of the last record returned. The next request fetches records strictly after that position.

Tombstones

Deleted records aren’t removed from the database — they’re soft-deleted with a deleted_at timestamp. This is essential for sync:
{
  "tombstones": [
    {
      "clientBookmarkId": "def-456",
      "deletedAt": "2026-04-12T11:00:00Z"
    }
  ]
}
Without tombstones, clients would never know a record was deleted and would keep showing stale data.
Tombstones appear in delta responses alongside upserts. A record’s deleted_at timestamp determines its position in the sync stream — it’s treated like any other update.

Client Sync Algorithm

1

Load cursor

Read the stored nextCursor from local storage. Use null for first sync.
2

Fetch delta

GET /bookmarks/delta?cursor=<stored>&limit=100
3

Apply upserts

For each record in upserts:
  • If it exists locally → update local fields
  • If it’s new → insert into local store
  • Always merge server-authoritative fields (enrichment data)
4

Apply tombstones

For each record in tombstones:
  • Mark the local record as deleted
  • Remove from UI but optionally keep in local store for undo
5

Store cursor

Save nextCursor to local storage.
6

Paginate

If nextCursor is not null, repeat from Step 2. When null, sync is complete.

Conflict Resolution

UniSave uses server last-write-wins (LWW):
  • The server sets updated_at = now() on every successful write
  • When two clients modify the same record, the most recent server-side write wins
  • Clients don’t need to implement complex merge logic

Enrichment Merge Exception

Enrichment data (summary, tags, saveWhy, enrichmentStatus) is always server-authoritative. Even if a client’s local copy has a newer updated_at, enrichment fields from the server should always be accepted:
Client has: { title: "My Title", enrichmentStatus: "pending", updatedAt: T2 }
Server has: { title: "Old Title", enrichmentStatus: "completed", summary: "...", updatedAt: T1 }

Result: { title: "My Title", enrichmentStatus: "completed", summary: "..." }
         (client title wins by LWW, enrichment data always from server)

Offline Queue

When a client is offline, writes are queued locally:
  1. User saves/edits/deletes a bookmark → written to local store + offline queue
  2. UI updates immediately from local store
  3. When connectivity resumes, the offline queue replays writes to the server
  4. Server applies LWW — if the record was modified on another device, the most recent write wins
  5. Delta sync fetches any server-side changes missed during offline period

Sync Triggers

Clients trigger delta sync in these situations:
TriggerContext
App launchInitial catch-up sync
Pull to refreshUser-initiated
WebSocket nudgebookmarks_changed event (when available)
Background synciOS BGTaskScheduler periodic refresh
After offline queue flushEnsure local and server are consistent

Data Consistency Guarantees

  • Eventual consistency — all clients converge to the same state, given connectivity
  • No data loss — offline writes are queued and replayed; tombstones ensure deletes propagate
  • Idempotent upserts — PUT operations can be safely retried without side effects
  • Monotonic cursors — cursors always move forward; replaying a cursor never skips records