Contents

brightdigit/mistkit

A Swift Package for Server-Side and Command-Line Access to CloudKit Web Services

Table of Contents

- Key Features

- Installation - Requirements - Platform Support - Quick Start

- Authentication - Error Handling - Advanced Usage - Examples

- Guides - CloudKit as Your Backend (talk)

Overview

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.

Key Features

  • 🌍 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

Why Server-Side CloudKit?

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.

Getting Started

Installation

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:

  1. File β†’ Add Package Dependencies
  2. Enter: https://github.com/brightdigit/MistKit.git
  3. Select version and add to your target

Requirements

  • Swift 6.1+
  • Xcode 16.0+ (for iOS/macOS development)
  • Linux: Ubuntu 18.04+ with Swift 6.1+

Platform Support

Minimum Platform Versions

| Platform | Minimum Version | |----------|-----------------| | macOS | 11.0+ | | iOS | 14.0+ | | tvOS | 14.0+ | | watchOS | 7.0+ | | visionOS | 1.0+ | | Linux | Ubuntu 18.04+ | | Windows | 10+ |

Quick Start

1. Choose Your Authentication Method

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.

API Token (read-only against the public database)
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
)
Web Authentication (user-context routes, private/shared database)
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
)
Server-to-Server (public database only)
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.

2. Call an Operation (database chosen per call)
let result = try await service.queryRecords(
    Query(recordType: "Post"),
    database: .public(.prefers(.serverToServer))
)
let records = result.records

Database.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.

Usage

Authentication

API Token Authentication
  1. Get API Token:

- Log into the CloudKit Console - Navigate to CloudKit Database - Generate an API Token

  1. Set Environment Variable:

``bash export CLOUDKIT_API_TOKEN="your_api_token_here" ``

  1. Use in Code:

``swift 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

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

Server-to-server authentication provides enterprise-level access using ECDSA P-256 key signing. Note that this method only supports the public database.

  1. Generate Key Pair:

```bash # 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 ```

  1. Upload Public Key: Upload the public key to Apple Developer Console
  1. Use in Code (the simplest path β€” Credentials resolves the PEM at first use):

```swift 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 the tokenManager: initializer instead:

``swift 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 ) ``

Error Handling

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)")
}
Error Types
  • CloudKitError: CloudKit Web Services API errors (typed throws on every operation)
  • CredentialsValidationError: Surfaces when Credentials.init is called with neither apiAuth nor serverToServer
  • TokenManagerError: Authentication and credential errors
  • TokenStorageError: Token storage and persistence errors

Advanced Usage

More Operations

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
)
Change Tracking

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/changes is deprecated by Apple in favor of changes/database, so fetchZoneChanges / fetchAllZoneChanges are marked @available(*, deprecated). Use fetchDatabaseChanges instead.

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)")
}
Auto-Chunking Conveniences

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
)
HTTP Transport

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.

Adaptive Token Manager

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)

Examples

Check out the Examples/ directory for complete working examples:

Documentation

Guides

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.

Articles on brightdigit.com: Rebuilding MistKit with Claude Code, part 1 and part 2.

CloudKit as Your Backend (talk)

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:

Leo Dion / BrightDigit
Use case examples
  • 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 workflow - cloudkit-sync composite action

What is CloudKit
CloudKit Web Services

- 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
Authentication
Swift OpenAPI Generator
Field types and error handling

- Broken call, CloudKit Web Services: Discovering All User Identities (GET users/discover) - Broken call, CloudKit JS: CloudKit.Container.discoverAllUserIdentities

Deployment
Other tools mentioned
  • Hummingbird β€” server behind the MistDemo web interface
  • Vapor β€” Heartwitch backend
  • OBS Studio β€” Heartwitch streaming overlay

Apple References

Related Swift Packages

License

MistKit is released under the MIT License. See LICENSE for details.

Acknowledgments

Roadmap

v1.1.0

Support


MistKit: Bringing CloudKit to every Swift platform 🌟

Package Metadata

Repository: brightdigit/mistkit

Default branch: main

README: README.md