Downloading every record on every launch wastes time and still handles deletion poorly. Incremental synchronization needs a durable position and an explicit deletion contract.
The server returns changed records, deleted IDs, and the next cursor:
struct ChangePage<Item: Decodable>: Decodable {
let items: [Item]
let deletedIDs: [UUID]
let nextCursor: String?
let hasMore: Bool
}
Apply one page in a local transaction: upsert items, apply tombstones, then store the cursor. Advancing the cursor before local persistence succeeds can permanently skip data.
Continue while hasMore, but bound each run so background execution can stop safely. The next launch resumes from the committed cursor.
Push and pull require conflict rules. Stable record IDs make retries idempotent. Updated timestamps alone are insufficient when device clocks disagree, so prefer server versions or server-assigned ordering where correctness depends on sequence.
Tombstones need retention. Keep them long enough for every supported client to observe the deletion, then compact them through an explicit policy.
Test duplicate pages, a crash before cursor commit, deletion followed by recreation, and an expired cursor requiring a full resync. A sync engine is a small distributed system; durable progress matters as much as data transfer.