A Swift Package for Server-Side and Command-Line Access to CloudKit Web Services
- Overview
- Why Server-Side CloudKit?
- Getting Started
- Usage
- Documentation
- License
- Acknowledgments
- Roadmap
- Support
MistKit provides a modern Swift interface to CloudKit Web Services REST API, enabling cross-platform CloudKit access for server-side Swift applications, command-line tools, and platforms where the CloudKit framework isn't available.
Built with Swift concurrency (async/await) and designed for modern Swift applications, MistKit supports all three CloudKit authentication methods and provides type-safe access to CloudKit operations.
- 🌍 Cross-Platform Support: Works on macOS, iOS, tvOS, watchOS, visionOS, Linux, and Windows
- ⚡ Modern Swift: Built with Swift 6 concurrency features and structured error handling
- 🔐 Multiple Authentication Methods: API token, web authentication, and server-to-server authentication
- 🛡️ Type-Safe: Comprehensive type safety with Swift's type system
- 📋 OpenAPI-Based: Generated from CloudKit Web Services OpenAPI specification using swift-openapi-generator
- 🔒 Secure: Built-in security best practices and credential management
Apple's CloudKit framework only runs on Apple platforms. MistKit wraps the CloudKit Web Services REST API so server-side Swift, Linux services, and command-line tools can take part in the same containers as your apps. Four patterns cover most uses:
- Public database as a managed catalog — a scheduled job writes data every user wants and the app just queries it. BushelCloud (
Examples/BushelCloud) syncs macOS restore images and Xcode/Swift versions for Bushel; CelestraCloud (Examples/CelestraCloud) syncs RSS feeds for Celestra. Software-version catalogs, asset packs, feature flags, and MDM configuration fit the same shape. - Private database on behalf of a user — the user signs in once, the server keeps their web auth token, and reads or writes their private database while they are away. HeartWitch links an Apple Watch to a Vapor backend this way; wearable data pipelines, two-way sync with external services, and server-side processing of uploads are the same idea.
- Web app ↔ Apple device bridge — a browser portal for a CloudKit-backed app, or a webhook handler that writes straight into a user's records.
- Data aggregation — anonymized telemetry read through
records/changes, or crowdsourced data cleaned up by a background job.
The talk that walks through all of this is CloudKit as Your Backend below.
Add MistKit to your Package.swift:
dependencies: [
.package(url: "https://github.com/brightdigit/MistKit.git", from: "1.0.0-beta.5")
]Or add it through Xcode:
- File → Add Package Dependencies
- Enter:
https://github.com/brightdigit/MistKit.git - Select version and add to your target
- Swift 6.1+
- Xcode 16.0+ (for iOS/macOS development)
- Linux: Ubuntu 18.04+ with Swift 6.1+
| Platform | Minimum Version |
|---|---|
| macOS | 11.0+ |
| iOS | 14.0+ |
| tvOS | 14.0+ |
| watchOS | 7.0+ |
| visionOS | 1.0+ |
| Linux | Ubuntu 18.04+ |
| Windows | 10+ |
MistKit supports three credential types via the Credentials value. The service
does not carry a database — each operation picks its database (and signing
method, for the public database) at the call site.
import MistKit
let credentials = try Credentials(
apiAuth: APICredentials(
apiToken: ProcessInfo.processInfo.environment["CLOUDKIT_API_TOKEN"]!
)
)
let service = CloudKitService(
containerIdentifier: "iCloud.com.example.MyApp",
credentials: credentials
)let credentials = try Credentials(
apiAuth: APICredentials(
apiToken: ProcessInfo.processInfo.environment["CLOUDKIT_API_TOKEN"]!,
webAuthToken: userWebAuthToken
)
)
let service = CloudKitService(
containerIdentifier: "iCloud.com.example.MyApp",
credentials: credentials
)let credentials = try Credentials(
serverToServer: ServerToServerCredentials(
keyID: ProcessInfo.processInfo.environment["CLOUDKIT_KEY_ID"]!,
privateKey: .file(path: "private_key.pem")
)
)
let service = CloudKitService(
containerIdentifier: "iCloud.com.example.MyApp",
credentials: credentials,
environment: .production
)Provide both apiAuth and serverToServer to a single Credentials when one
service must hit public-database routes via S2S signing and user-context
routes via web-auth — MistKit picks the appropriate token manager per call.
let result = try await service.queryRecords(
Query(recordType: "Post"),
database: .public(.prefers(.serverToServer))
)
let records = result.recordsDatabase.public carries a PublicAuthPreference:
.prefers(.serverToServer) / .prefers(.webAuth) (fall back if not configured)
or .requires(.serverToServer) / .requires(.webAuth) (throw if not configured).
Private/shared always use web-auth.
-
Get API Token:
- Log into the CloudKit Console
- Navigate to CloudKit Database
- Generate an API Token
-
Set Environment Variable:
export CLOUDKIT_API_TOKEN="your_api_token_here"
-
Use in Code:
let credentials = try Credentials( apiAuth: APICredentials( apiToken: ProcessInfo.processInfo.environment["CLOUDKIT_API_TOKEN"]! ) ) let service = CloudKitService( containerIdentifier: "iCloud.com.example.MyApp", credentials: credentials )
Web authentication enables user-specific operations and requires both an API token and a web authentication token. The token can be obtained either through CloudKit JS authentication (browser flow) or from an iOS/macOS app via CKFetchWebAuthTokenOperation, which exchanges the user's existing iCloud session for a token your backend can use.
let credentials = try Credentials(
apiAuth: APICredentials(apiToken: apiToken, webAuthToken: webAuthToken)
)
let service = CloudKitService(
containerIdentifier: "iCloud.com.example.MyApp",
credentials: credentials
)Server-to-server authentication provides enterprise-level access using ECDSA P-256 key signing. Note that this method only supports the public database.
-
Generate Key Pair:
# Generate private key openssl ecparam -genkey -name prime256v1 -noout -out private_key.pem # Extract public key openssl ec -in private_key.pem -pubout -out public_key.pem
-
Upload Public Key: Upload the public key to Apple Developer Console
-
Use in Code (the simplest path —
Credentialsresolves the PEM at first use):let credentials = try Credentials( serverToServer: ServerToServerCredentials( keyID: "your_key_id", privateKey: .file(path: "private_key.pem") ) ) let service = CloudKitService( containerIdentifier: "iCloud.com.example.MyApp", credentials: credentials, environment: .production ) // Each call selects its database scope explicitly: let records = try await service.queryRecords( Query(recordType: "Post"), database: .public(.requires(.serverToServer)) ).records
To plug in a custom
TokenManager(e.g. with shared connection pooling), use thetokenManager:initializer instead:let pemString = try String(contentsOfFile: "private_key.pem", encoding: .utf8) let serverManager = try ServerToServerAuthManager( keyID: "your_key_id", pemString: pemString ) let service = CloudKitService( containerIdentifier: "iCloud.com.example.MyApp", tokenManager: serverManager, environment: .production )
MistKit provides comprehensive error handling with typed errors:
do {
let credentials = try Credentials(
apiAuth: APICredentials(apiToken: apiToken)
)
let service = CloudKitService(
containerIdentifier: "iCloud.com.example.MyApp",
credentials: credentials
)
// Perform operations — each call picks its database, e.g.:
let posts = try await service.queryRecords(
Query(recordType: "Post"),
database: .public(.prefers(.serverToServer))
).records
} catch let error as CloudKitError {
print("CloudKit error: \\(error.localizedDescription)")
} catch let error as TokenManagerError {
print("Authentication error: \\(error.localizedDescription)")
} catch let error as CredentialsValidationError {
print("Credentials error: \\(error.localizedDescription)")
} catch {
print("Unexpected error: \\(error)")
}CloudKitError: CloudKit Web Services API errors (typed throws on every operation)CredentialsValidationError: Surfaces whenCredentials.initis called with neitherapiAuthnorserverToServerTokenManagerError: Authentication and credential errorsTokenStorageError: Token storage and persistence errors
Beyond querying and CRUD, MistKit covers zones, subscriptions, push tokens, and
asset re-referencing. Every call takes an explicit database:.
// Zones
let zone = try await service.createZone(
zoneName: "Notes",
database: .private
)
try await service.deleteZone(zoneName: "Notes", database: .private)
// Batch create/delete via service.modifyZones(_:database:)
// (takes [ZoneOperation], returns [ZoneChangeResult] — inspect
// `.zones` and `.failures` for per-zone outcomes).
// Subscriptions
let subs = try await service.listSubscriptions(database: .private)
let one = try await service.lookupSubscriptions(ids: ["sub-1"], database: .private)
// Create/update/delete via service.modifySubscriptions(_:database:)
// (takes [SubscriptionOperation], returns [SubscriptionResult]).
// APNs push tokens
let token = try await service.createAPNsToken(
environment: .development,
database: .private
)
try await service.registerAPNsToken(
token.apnsToken,
environment: .development,
database: .private
)
// Re-reference existing CDN assets without re-uploading bytes
let assets = try await service.rereferenceAssets(
[(recordName: "rec-1", fieldName: "photo")],
database: .private
)CloudKit exposes four change-tracking endpoints. MistKit wraps all four; each
single-request primitive has an auto-paginating fetchAll… companion.
| Apple endpoint | Purpose | MistKit method | Auto-paginating |
|---|---|---|---|
records/changes |
Fetching Record Changes | fetchRecordChanges |
fetchAllRecordChanges |
changes/database |
Fetching Database Changes — which zones changed | fetchDatabaseChanges |
fetchAllDatabaseChanges |
changes/zone |
Fetching Record Zone Changes — records within zones | fetchRecordZoneChanges |
fetchAllRecordZoneChanges |
zones/changes |
Fetching Zone Changes — deprecated by Apple | fetchZoneChanges |
fetchAllZoneChanges |
zones/changesis deprecated by Apple in favor ofchanges/database, sofetchZoneChanges/fetchAllZoneChangesare marked@available(*, deprecated). UsefetchDatabaseChangesinstead.
The typical database-sync flow asks which zones changed, then fetches the records inside them:
// 1. Which zones changed?
let database = try await service.fetchDatabaseChanges(
syncToken: lastDatabaseToken,
database: .private
)
// 2. What changed inside them?
let result = try await service.fetchAllRecordZoneChanges(
zones: database.changedZones.map {
ZoneChangesRequest(zoneID: ZoneID(zoneName: $0.zoneName))
},
database: .private
)
for change in result.changes {
print("\(change.zone.zoneName): \(change.records.count) changed")
// Persist change.syncToken per zone — each zone paginates independently.
}Both operations report per-zone problems as data rather than throwing, so one bad zone never discards the zones that succeeded:
for failure in result.failures {
print("\(failure.zoneName) failed: \(failure.serverErrorCode.rawValue)")
}CloudKit caps batch requests at 200 items. lookupAllRecords and the
lookupInfos: form of discoverAllUserIdentities split oversized inputs into
≤maxRecordsPerRequest (200) batches automatically and concatenate the results
in input order — no manual chunking required.
let records = try await service.lookupAllRecords(
recordNames: thousandsOfNames, // chunked into 200-item requests
database: .private
)
let identities = try await service.discoverAllUserIdentities(
lookupInfos: manyLookupInfos,
batchSize: 200
)Non-WASI platforms default to URLSessionTransport — no transport plumbing is
required. On Apple platforms, the default convenience initializer used in the
examples above wires up URLSessionTransport automatically.
WASI builds use the generic, transport-accepting initializer; see
Sources/MistKit/CloudKitService/CloudKitService+Initialization.swift for the
internal entry point. A custom transport on Apple platforms (e.g. for
server-side Swift with AsyncHTTPClient) is not yet exposed in the public
v1.0.0-beta surface — track via the project roadmap.
For applications that might upgrade from API-only to web authentication:
let adaptiveManager = AdaptiveTokenManager(
apiToken: apiToken,
storage: storage
)
// Later, upgrade to web authentication
try await adaptiveManager.upgradeToWebAuthentication(webAuthToken: webToken)Check out the Examples/ directory for complete working examples:
- MistDemo: Web-based CloudKit authentication demo with automatic token capture
- BushelCloud (standalone repo): Server-to-Server auth demo syncing macOS restore images, Xcode, and Swift versions from a scheduled GitHub Actions job — backend for the Bushel app
- CelestraCloud (standalone repo): RSS reader demonstrating CloudKit query filtering, sorting, and web etiquette patterns — backend for the Celestra app, built with SyndiKit
- API Documentation: Complete API reference, hosted on Swift Package Index
The DocC catalog (Sources/MistKit/Documentation.docc/) carries the long-form guides. Links below point at the published pages; pages added on this branch appear once Swift Package Index rebuilds the default branch.
- CloudKit as Your Backend: the conference talk in article form — see below
- Authentication and Databases: which credentials reach which database, obtaining tokens and keys from the CloudKit Console
- Request Signing: token managers, authenticators, the middleware, and the ECDSA payload
- Working with Records: query, create, update, delete, batch, and sync
- Field Type Polymorphism: how nine CloudKit field types map onto one Swift enum, and the wire-format traps
- Handling Errors: typed errors at every layer and how CloudKit's JSON becomes a
CloudKitError - Deploying MistKit: static Linux builds, credentials in CI, scheduling, idempotency, observability
- Configuring MistKit and CloudKit Limits and Performance
- Abstraction Layer Architecture, OpenAPI Code Generation, Generated Code Workflow, Generated Code Analysis
- What CloudKit Got Wrong: where Apple's documentation and Apple's server disagree
- What the AI Got Wrong: an evidence-backed catalogue of AI-assisted development failure modes from this project
Articles on brightdigit.com: Rebuilding MistKit with Claude Code, part 1 and part 2.
From iOS to Server-Side Swift — given in 2026 at Swift Craft and iOSDevUK by Leo Dion (@leogdion@c.im). The full article, following the slide order with screenshots and code from this repository, is in the DocC catalog: CloudKit as Your Backend (source: CloudKitAsYourBackend.md). Download the slides: CloudKit-Backend-iOSDevUK.pdf (~12 MB).
CloudKit has excellent documentation for iOS and macOS client development. But backend services — podcast aggregation, RSS readers, data processing — face APIs that Apple barely documents. I rebuilt a comprehensive CloudKit library using AI-generated OpenAPI specifications. The result: type-safe Swift code supporting three authentication methods (server-to-server, web authentication token, and API token), typed error handling, and production deployments.
Links from the talk:
- MistKit on GitHub and MistKit issues — "What's next?"
- MistDemo — CLI, macOS app and web demo used for integration testing
- Bushel — Virtualization for App Developers
- AtLeast — Passive Timer for Apple Watch
- Heartwitch — Apple Watch heart-rate streaming; App Store
- BrightDigit
- linktr.ee/leogdion
- iOSDevUK
- BushelCloud — server-to-server sync of macOS restore images, Xcode and Swift versions for Bushel; the GitHub Actions deployment shown in the talk (also at
Examples/BushelCloud)cloudkit-sync-dev.yml— scheduled workflowcloudkit-synccomposite action
- CelestraCloud — RSS feed sync into a public database for Celestra, with query filtering and sorting (also at
Examples/CelestraCloud) - HeartWitch — the private-database, Apple Watch to Vapor bridge
- Rebuilding MistKit with Claude Code, part 1 and part 2
- WWDC 2014 "Introducing CloudKit" (session 208) — Apple no longer hosts the video; this mirror has the video, slides and transcript. Transcript only: ASCIIwwdc
- CloudKit framework documentation
- Enabling CloudKit in your app — Xcode capability setup
- CloudKit Console
- Integrating a text-based schema into your workflow
- cktool and Automating CloudKit Development
- CloudKit JS
- CloudKit Web Services Reference — archived
- Composing Web Service Requests
- Accessing CloudKit Using an API Token
- Accessing CloudKit Using a Server-to-Server Key
- Document Revision History — last updated 2016
- Uploading Assets
- Types and Dictionaries — field types
- Error Codes
- Discovering User Identities (POST users/discover)
- Discovering All User Identities (GET users/discover) — the endpoint that returns HTTP 500
- Base URL:
https://api.apple-cloudkit.com/database/{version}/{container}/{environment}/{database}/{operation} - Apple Developer Support — contact URL in the OpenAPI document
- CKFetchWebAuthTokenOperation
- Sign in with Apple — mentioned as not existing when Heartwitch was built
- swift-crypto —
P256.Signing.PrivateKeyused inRequestSignature - MistKit source shown in the talk:
AuthenticationMiddleware.swift,APITokenAuthenticator.swift,WebAuthTokenAuthenticator.swift,ServerToServerAuthenticator.swift,RequestSignature.swift
- swift-openapi-generator
- WWDC23 "Meet Swift OpenAPI Generator"
- Documentation and tutorial: Working with Swift OpenAPI Generator
- ClientMiddleware — the "Implement a custom client middleware" bearer-token example is a section on that page
- Example projects — including auth, logging and retrying middleware examples
- Package ecosystem: swift-openapi-runtime, swift-openapi-urlsession, swift-openapi-async-http-client, swift-okhttp, swift-openapi-vapor, swift-openapi-hummingbird, swift-openapi-lambda
- OpenAPI Specification
- MistKit
openapi.yaml
FieldValue.swift- MistKit issue #28: discoverAllUserIdentities returns HTTP 500
- Broken call, CloudKit Web Services: Discovering All User Identities (GET users/discover)
- Broken call, CloudKit JS:
CloudKit.Container.discoverAllUserIdentities
- Apple Feedback FB22754466 — public copy on Open Radar; filed via Feedback Assistant
- GitHub Actions: scheduled workflows (
schedule/ cron) - GitHub Actions: using secrets
- GitHub Actions: creating a composite action
- actions/checkout
- dawidd6/action-download-artifact
- Swift Docker images — the sync action builds with
swiftlang/swift:nightly-6.4.x-noble - swift-configuration — how the example CLIs read configuration from environment variables or arguments
- MistKitConfiguration — the shared credential-configuration package built on it (also at
Packages/MistKitConfiguration)
- Hummingbird — server behind the MistDemo web interface
- Vapor — Heartwitch backend
- OBS Studio — Heartwitch streaming overlay
- CloudKit Web Services: Official CloudKit Web Services REST API documentation
- CloudKit framework: On-device CloudKit framework (iOS/macOS)
- CloudKit JS: Browser-based CloudKit access used for web auth token capture
- CKFetchWebAuthTokenOperation: iOS/macOS API for exchanging an iCloud session for a web auth token
- swift-openapi-generator: Generates type-safe Swift clients from OpenAPI specs
- swift-openapi-async-http-client: AsyncHTTPClient transport for OpenAPI clients
- AsyncHTTPClient: HTTP client for server-side Swift
- swift-crypto: Cross-platform crypto used for ECDSA P-256 server-to-server signing
MistKit is released under the MIT License. See LICENSE for details.
- Built on Swift OpenAPI Generator
- Uses Swift Crypto for server-to-server authentication
- Inspired by CloudKit Web Services REST API
- Add CloudKit Schema Management APIs (cktool/cktooljs functionality)
- Add KeyPath-based QueryFilter API for Type-Safe Filtering
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation: API Reference
MistKit: Bringing CloudKit to every Swift platform 🌟