Short answer
Duplicates appear when two devices create the same thing before syncing, and CloudKit can't enforce uniqueness to stop them. Local CRM merges afterwards: tags with the same name (ignoring case and accents), contacts linked to the same outside record such as a Shopify shop, and timeline entries with the same event ID. The oldest record is kept, and everything attached to the copy moves onto it. Contacts are never merged just because their names match.
With iCloud sync on, every device you use keeps the whole Local CRM database and works offline. That's why the app is fast on a plane and why your data never passes through a server of ours. It also means two devices can each create the same thing while neither knows about the other: a “VIP” tag on your iPhone on the train, a “vip” tag on your Mac at the office. When they next sync, both arrive.
Why not prevent it?#
With a server, preventing duplicates is simple: before creating a tag, ask the server whether it already exists. Local CRM has no server to ask, by design. Its data is stored with SwiftData, which syncs through the private database of your iCloud account using CloudKit, and a CloudKit-backed store can't enforce uniqueness: there's no way to declare “only one tag per name” and have it hold across devices that are offline.
So duplicates can't be prevented. They have to be recognized and merged, safely, on each device, after the fact.
iPhone offline
Mac offline
The merge rules#
Local CRM looks for four kinds of duplicates. For each, the question is the same: what proves two records are the same thing?
| Record | Same thing when | Kept | What moves to the kept record |
|---|---|---|---|
| Tag | Same name, ignoring case and accents | The oldest tag | Every contact tagged with the copy |
| Contact | Linked to the same outside record, like a Shopify shop ID | The contact with the oldest link | Notes, todos, timeline entries, tags and related people; missing details are filled in |
| Timeline entry | Same event ID | The first one stored | Nothing |
| Plugin settings | Same plugin | The most recently updated | Nothing |
When two contacts are merged, the kept contact's own values win. The copy only fills gaps: a missing job title, birthday or photo comes from it, and phone numbers, email addresses and postal addresses are combined without repeats, comparing phone numbers by their digits. The earlier creation date and the later last-interaction date are kept, so Insights still knows when you last talked.
Tag names are compared with the same folding Local CRM's search uses, so “Café” and “cafe” are one tag. How folding works, and where it stops.
When merging runs#
Merging is safe to run at any time, because running it twice changes nothing the second time. Local CRM runs it:
- when it starts;
- after each plugin sync, such as the Shopify Partners plugin importing installs on two devices;
- a few seconds after changes arrive from another device;
- whenever you choose Settings → Data → Merge Duplicates.
What we chose not to merge#
Two contacts named Alex Kim, created separately on two devices, stay two contacts. A name is a guess about identity; a shop ID is proof. The same goes for a shared phone number or email address: an office line or a team inbox can belong to several people. Merging the wrong two people would mix their notes and history in a way that's very hard to untangle, while two copies of the same person are easy to spot and fix.
If you build sync without a server#
- Expect duplicates. With offline devices and no coordinator, they'll happen. Design the merge, not a lock.
- Merge only on proof. Use identifiers you control or an outside system issued; never similarity.
- Keep the oldest. Links people already made to the older record keep working.
- Make it repeatable. A merge that's safe to run twice can run at launch, after every sync, and from a button.
Turning on iCloud sync and what it changes is covered on the Local CRM page and in the FAQ.
Questions this note answers
- Why did a tag appear twice after I turned on iCloud sync?
- Two of your devices created it before they had synced, so both copies arrived. Local CRM merges them a few seconds after the changes come in, keeping the oldest.
- Which record does Local CRM keep when it merges duplicates?
- The oldest. Notes, todos, timeline entries and tags attached to the newer copy move onto it, and fields the oldest one is missing are filled in from the copy.
- Will Local CRM merge two contacts with the same name?
- No. Two people can share a name, so contacts are only merged when they're linked to the same outside record, such as the same Shopify shop.
- Can I merge duplicates myself?
- Yes. Settings → Data → Merge Duplicates runs the same check whenever you like.
Select any passage to quote it with a link straight to it.