SwiftData and CloudKit: syncing without a server of your own
Plenty of apps do not need a server of their own. What they need is a way to keep one person’s data identical on their iPhone, their iPad and whatever device comes next. For our own apps, ContoTrip and Avantido, we therefore chose SwiftData with CloudKit. The data lives locally on the device and is synchronised through the user’s private iCloud database. We run no database, no API and no server for it – and we have no access to that data.
This post explains why we work this way, what it looks like in practice and where the lessons can be expensive.
Why no server?
A backend of your own means operations: updates, backups, monitoring, credentials, privacy documentation and a single place where the data of many people piles up. For a travel expense or to-do app, that is a high price for a feature Apple already ships.
CloudKit shifts the responsibility. The data sits in the private database of each Apple account. As the provider, we never see it, and we do not have to protect it, because it never reaches us. That simplifies the privacy policy and turns the promise “your data stays with you” into something that can be verified technically.
How the app is structured
The data model is built with SwiftData using @Model classes. On launch, the app creates a container that is mirrored to CloudKit. If no iCloud account is signed in on the device, or iCloud is switched off for the app, it falls back to purely local storage. The app remains fully usable; only the sync is missing.
We build our apps with Swift 6 and strict concurrency checking. The project is described with XcodeGen in a project.yml; the Xcode project file is generated from it and never edited by hand. That keeps changes to capabilities and entitlements traceable – and with CloudKit, those matter a great deal.
A data model designed for sync
- Amounts as integer cents. Floating-point numbers are unsuitable for money, and syncing between devices does not make them any better.
- Capture values at the moment of entry. ContoTrip freezes the exchange rate when an expense is recorded, so totals stay stable no matter which device shows them, or when.
- Keep device-local state out of the model. Anything that only applies to one device does not belong in the synced data.
Pitfalls we ran into
iCloud containers cannot be deleted
This is the most important lesson. App IDs and app groups can be removed again in the developer portal; iCloud containers cannot. Register the name once under the wrong developer team – say, with a personal account for the first test build instead of the company account – and it is blocked there for good. That is exactly what happened with Avantido, which is why its container carries the suffix .data. Since then our rule is simple: the very first device build runs against the team the app will be published under.
No push, no instant sync
Without the aps-environment entitlement, CloudKit synchronises when the app launches and when it returns to the foreground. A change made on the iPad therefore does not show up on the iPhone within the same second, but the next time the app is opened. For our apps that is acceptable, as they contain no push code anyway. If you need instant sync, you have to add the Push Notifications capability deliberately.
The privacy label
Data in the private iCloud database is not accessible to the provider and, according to Apple, does not have to be declared in the privacy nutrition label. That is a genuine advantage. It does not, however, release you from honestly declaring other transfers – for example when an AI feature sends content to a provider.
Where the approach ends
CloudKit’s private database is designed for one person with several devices. A shared household, or a team working on the same data, needs more than that. We deliberately solve this differently: sharing means sending something to someone – as a file through the share sheet, via AirDrop, mail or a messenger. The file uses a versioned format and includes a readable text fallback for recipients who do not have the app.
CloudKit is also an Apple technology. An Android app cannot reach that data. For products meant to exist on both platforms, the shared exchange format is the point of contact, not a shared cloud.
When we recommend this route
- The app manages the personal data of a single person.
- There is no business reason to analyse the data centrally.
- The product should work without a user account or sign-in.
- Privacy is part of the product promise.
If a project instead needs several users working together, reporting or a web interface, we plan a dedicated backend – built with Symfony and a clear API. We make that call at the very beginning, because changing the architecture later is expensive.