iCloud transport
Instead of a server, the reference app can sync through the user's iCloud account. The data model and the merge rules are the same; only the transport differs. This page describes the mapping so that other Apple-platform clients can join an inventory shared through iCloud, and so that implementers understand what both transports have in common.
| Server | iCloud | |
|---|---|---|
| Record identity | (entity, id) |
CloudKit record name <entity>~<id> |
| Version | hlc |
hlc field, same format |
| Merge | the server and every client apply the merge rules | every client applies the merge rules; CloudKit only stores the latest saved version |
| Change feed | server change log and cursor | CloudKit change tokens, managed by CKSyncEngine |
| Attachments | content-addressed HTTP endpoints | LVEAttachment records with a CKAsset |
| Authentication | tokens, pairing | the user's Apple Account |
| Sharing | users on the same server | a zone-wide CKShare |
| Number ranges | server reservation | not available; device numbering |
| Locked templates | enforced by the server | not enforced; clients only show them read-only |
Container and zone
Section titled “Container and zone”| Item | Value |
|---|---|
| Container | iCloud.com.dxtsystems.lve |
| Zone name | LVE |
| Owner | the zone in the owner's private database, created by the owner's app on first sync |
| Member | the same zone, reached through the member's shared database with the owner's user record name as zone owner |
The app uses CKSyncEngine with one engine per device and role, and persists the engine state between launches.
Record types
Section titled “Record types”LVERecord
Section titled “LVERecord”One CloudKit record per protocol record, holding the same envelope as a change.
| Item | Value |
|---|---|
| Record type | LVERecord |
| Record name | <entity>~<id> with the id in upper case, for example article~8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13 |
entity (String) |
entity type, as in the change |
hlc (String) |
version stamp |
deleted (Int64) |
1 for a tombstone, 0 otherwise |
payload (Bytes) |
the payload as UTF-8 JSON bytes; absent for tombstones |
Tombstones are LVERecords with deleted = 1. CloudKit record deletions are never used, so a deleted record keeps its stamp and older versions cannot bring it back.
LVEAttachment
Section titled “LVEAttachment”| Item | Value |
|---|---|
| Record type | LVEAttachment |
| Record name | att~<sha256> with the lowercase hexadecimal SHA-256 of the file |
file (Asset) |
the file |
The same keys as on the server reference attachments (see Attachments). An attachment record is saved together with every record change that references it; saving an existing attachment again changes nothing. A receiving client stores the asset's bytes under their SHA-256.
LVEPlan
Section titled “LVEPlan”| Item | Value |
|---|---|
| Record type | LVEPlan |
| Record name | plan~team, in the owner's LVE zone |
teamActive (Int64) |
1 while the owner's Team plan is active, 0 otherwise |
updatedAt (Date/Time) |
when the owner's app last changed teamActive |
The record tells members whether the owner's Team plan, which covers them, is active (see Plans).
Sending
Section titled “Sending”- The app hands every outbox entry to the engine as a pending save of
<entity>~<id>, plus a pending save ofatt~<sha256>for every attachment the change references and the device holds. - When the engine asks for the record, the app builds it from the current version of the record in its store (not from the outbox entry) and reuses the last known server record, so that the save carries CloudKit's change tag.
- A successful save acknowledges the outbox entries of that record with the saved stamp.
Conflicts
Section titled “Conflicts”CloudKit rejects a save whose change tag is outdated with serverRecordChanged and returns the server's record. The app then:
- merges the server version into its store with the merge rules,
- saves its own version again, now with the server's change tag, if its stamp is greater; otherwise it acknowledges its own entry, because the server version won.
This reproduces "greatest stamp wins" on top of CloudKit, whose own conflict handling would otherwise keep whichever save came last.
| CloudKit error | Handling |
|---|---|
serverRecordChanged |
merge, then save again only if the local stamp is greater |
zoneNotFound |
create the zone again and retry |
unknownItem |
forget the cached server record and retry as a new record |
networkFailure, networkUnavailable, serviceUnavailable, requestRateLimited, zoneBusy |
retry |
| anything else | mark the outbox entry rejected with CloudKit's error text |
Receiving
Section titled “Receiving”For every fetched record:
LVEAttachment: store the asset's bytes.LVERecord: build a change fromentity, the id from the record name,hlc,deletedandpayload, observe the stamp and merge it with the merge rules. Records withoutentityorhlcare skipped.
Movements keep their first version, as everywhere. Unknown entity types are stored and saved back unchanged.
Sharing with a team
Section titled “Sharing with a team”The owner shares the whole zone:
- The owner's app creates (or reuses) a zone-wide
CKShare(CKRecordNameZoneWideShare) with no public permission and a title, and presents the system sharing interface. Participants are invited by the owner through iCloud. - A participant opens the invitation. The app accepts the share and syncs as member of the owner's zone through its shared database.
- Owner and members read and write the same records; each merges with the same rules.
When the owner deletes the zone or stops sharing, the member's app reports that the share ended.
| Role | Needs |
|---|---|
| Owner, own devices only | Pro (or Team) on the owner's devices |
| Owner sharing with a team | Pro for the owner's own sync; Team so that members can sync |
| Member | nothing of its own; covered by the owner's Team plan |
Members cannot see the owner's purchases. The owner's app therefore records whether its Team plan is active in the LVEPlan record whenever the plan changes, but only after it has shared the inventory from that device. Members read the record at the start of every sync round:
- no record: treated as active,
- record could not be read: the last known value stays,
teamActive = 0: the member pauses sync until the value is1again.
Because this is updated only while the owner's app runs, members keep syncing until the owner's app has seen the plan end. A paused device keeps its data and its outbox; see When the licence lapses for the same behaviour with a server.
Switching transports
Section titled “Switching transports”Turning on iCloud disconnects a server connection (the token, cursor and reserved numbers are dropped) and queues every record for upload, as connecting to a server does. A device uses one transport at a time. To move an inventory from iCloud to a server, connect one device that holds all data to the server: it pushes everything, and other devices then connect to the server as well.