Skip to content

Latest commit

 

History

112 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MistKit Logo

MistKit

SwiftPM Swift Versions Platforms License GitHub Workflow Status Codecov Maintainability Documentation

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

Table of Contents

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
  2. Set Environment Variable:

    export CLOUDKIT_API_TOKEN="your_api_token_here"
  3. 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

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:

    # 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
  2. Upload Public Key: Upload the public key to Apple Developer Console

  3. Use in Code (the simplest path — Credentials resolves 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 the tokenManager: 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
    )

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

What is CloudKit

CloudKit Web Services

Authentication

Swift OpenAPI Generator

Field types and error handling

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 🌟

About

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

Topics

Resources

Stars

267 stars

Watchers

8 watching

Forks

Releases

Packages

Used by

Contributors

Languages