cloudkit-schema-source-of-truth
Committed `.ckdb` as the CloudKit schema source of truth, with `xcrun cktool` export / validate / import against Development and Production promotion kept as a Console-only gate. Use when a CloudKit-backed persistence layer adds or edits a record type, field, or index; before any
Install
npx skills add https://github.com/wei18/apple-dev-skills/tree/main/apple-dev-skills/skills/cloudkit-schema-source-of-truth
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install wei18-apple-dev-skills@llmmart
git clone https://github.com/wei18/apple-dev-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole wei18/apple-dev-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
CloudKit Schema Source of Truth
CloudKit has no migration-file system like a SQL database. The schema lives in Apple's
CloudKit Dashboard/Console, and xcrun cktool (Apple's official CLI, ships with Xcode) can
export, validate, and import it — but only against the Development environment.
Production promotion is a manual, irreversible Console action. This skill makes the
Development side of that workflow scriptable and commit-trackable while keeping the
Production gate correctly user-owned.
When to invoke
- A persistence change adds, renames, or edits a CloudKit record type, field, or index.
- You need to push schema to a container (Development or Production).
- Before any Production schema deploy — read the safety gate below first.
- Asked why a
.ckdbfile is committed to the repo, or why a field the app writes is missing from Production data.
Scope
Owns: the .ckdb-as-source-of-truth workflow, cktool invocations against Development, and
the Production promotion gate. Does not own: the Swift-side persistence/service code that
reads and writes CloudKit records → swift-dependency-injection for how that seam is
injected and faked in tests; secret storage for the management token itself →
apple-public-repo-security / build-time-secret-injection.
Prerequisites (one-time, user-owned)
Two credentials, kept in a gitignored env file (e.g. secrets/.env, with a committed
.env.example template):
- A CloudKit management token — generated by a human in CloudKit Dashboard → Settings → Tokens → Create Token (Management). This is a privileged credential; treat it like an API key with schema-write access, not like a build-time public identifier.
- The Apple Developer Team ID (10 characters).
The container identifier itself (e.g. iCloud.com.example.myapp) is not secret and can be
hardcoded in tooling.
Workflow
- Authenticate
cktoolfor this session (positional arg — see gotcha 1 below). - Export the live Development schema to the committed source-of-truth file (seed step first: run a debug build once so the app's JIT schema provisions the Development container, THEN export).
- Pre-flight: validate the committed
.ckdbagainst the live container before importing. - Deploy to Development — freely runnable and reversible.
- Always clear the token from
cktool's keychain store when done (via a shelltrap ... EXITaround steps 1–4, so it's purged even if a step fails midway).
Runnable as ${CLAUDE_SKILL_DIR}/scripts/ck-schema-dev.sh (the scripts/ folder next to this SKILL.md) — run it by that path or copy it into your repo's scripts/; see that file for the exact cktool invocations and flags.
Inputs / outputs
- Input: the credentials above, plus either the live Development container (
export) or the committedcloudkit/<app>.ckdb(validate/import). - Output:
exportoverwritescloudkit/<app>.ckdb— review the diff, then commit it as the schema source of truth.importmutates the named container's live schema. .ckdbfiles are not secrets — they contain schema definitions only, no data or tokens — so they're committed like any other source file, distinct from the token itself.
Safety gate — Production promotion is user-owned, Console-only, irreversible
cktool cannot push schema to Production. import-schema --environment production
rejects with an "endpoint not applicable in this environment" style error, and there is no
promote subcommand. The Development → Production promotion happens only in the CloudKit
Console:
- Bring Development fully in sync first (
import-schema --environment developmentabove). - Console → your container → environment Development → Schema → "Deploy Schema Changes to Production…" → review the generated field/index diff → confirm the deploy.
CloudKit Production record types and fields are add-only by Apple's own rule — once
deployed they can't be deleted or renamed, only added to. Indexes are different: they can be
added and removed in Production (WWDC21 "Automate CloudKit tests with cktool and declarative
schema"). Restricting the promotion path to the
Console keeps it naturally user-owned: automation prepares and validates the .ckdb and the
Development deploy; a human clicks the actual Production button.
export / validate / import --environment development are all reversible and safe to run
repeatedly without asking anyone.
Live-run gotchas
save-tokentakes the token as a positional argument, not piped stdin. Non-interactive stdin piping fails withError: Interaction was required while running in non-interactive mode (CKTOOL_NO_PROMPT=1 or not running in an interactive terminal).(verified locally, cktool 1.0.23001). Brief command-line argv exposure of the token is the tradeoff; purge it from the keychain store immediately after (see thetrapnote above).validate-schemarequires--environmentexplicitly — omitting it is a hard error, not a default.import-schemaonly ever targets Development. Don't assume a script that "runsimport-schema --environment production" has ever actually been exercised — smoke-test any such tooling against real credentials before trusting it; a plausible-looking Production import path that was never live-tested can sit broken for a long time undetected.- Just-in-time (JIT) schema exists only in Development. A debug build auto-creates record
types and fields the first time it writes them, in Development only — Production never does
this. Corollary: any field the app code writes that was never JIT-seeded in Development
before the last Console promotion is missing in Production, and the server returns a
CKErrorfor the unknown field when the app tries to save it — this only looks silent if the app's own save-completion handling swallows or ignores that error instead of surfacing it. Audit method:export-schema --environment productionto a scratch file and diff its field set against every field the code actually writes, and confirm every CloudKit save call actually surfaces its error instead of discarding it. - JIT marks every field it creates
QUERYABLE SEARCHABLE SORTABLE. A hand-authored.ckdbshould declare the minimal index set the app's actual queries need instead (e.g. only the one field a specific equality query filters on, asQUERYABLE) — every index adds query/storage cost and noise to the.ckdbdiff, so start minimal and add indexes as real queries need them. import-schemais a declarative import, so a.ckdbcan be hand-authored from scratch — no Dashboard clicking, no live seed build required. Use oneexport's output as the syntax template (it includes the system"___*"fields and theGRANTblock a hand-written file also needs).
For other cktool subcommands/flags not required for the core loop above (e.g. import-schema --validate, export-schema --output-file <path>), run xcrun cktool help / xcrun cktool <subcommand> --help — the offline, primary source, more current than any WWDC talk. One flag
worth knowing without opening a shell: reset-schema resets a container's Development
environment to match Production and deletes all Development data — a "start clean and
re-seed" tool, not part of the routine loop.
Idempotency
export: re-running always overwritescloudkit/<app>.ckdbwith the current Development schema — treat the file as generated + reviewed, not hand-edited, whenever a live export is the intended source.import: CloudKit's import is declarative — re-applying the same unchanged.ckdbis a no-op.
Rationale
Treating one .ckdb per app as the schema source of truth gives CloudKit the same
review-before-merge discipline a SQL migration file gets, despite CloudKit having no native
migration mechanism. Restricting the token to Development-only tooling, and Production to a
Console click, matches Apple's own irreversibility constraint (add-only fields) to a
correspondingly irreversible, deliberately manual approval step.
Deviation considerations
- A container with no meaningful schema evolution (fixed at launch, never touched again): a single manual export is enough; the ongoing export/validate/import loop isn't worth automating for a container that never changes.
- Multiple apps sharing one CloudKit container: keep one
.ckdbper container (not per app) and make the ownership of shared record types explicit in its surrounding docs, so two apps don't independently "fix" the same field in diverging ways.
Common Mistakes
- Assuming
import-schema --environment productionworks because it's syntactically accepted-looking — it is Development-only; Production is Console-only. - Skipping the Development JIT-seed step before an export — the export then reflects an
incomplete schema, and the gap resurfaces later as a Production write returning
CKErrorfor the unknown field, which looks like a silent failure only if that error is discarded. - Leaving the management token in
cktool's keychain store after a session — purge it even on script failure via atrap. - Hand-editing
.ckdbopportunistically without re-validating against the live Development container before importing. - Over-indexing a hand-authored
.ckdb(marking every fieldQUERYABLE SEARCHABLE SORTABLEout of caution) — every extra index adds query/storage cost and noise to the.ckdbdiff; declare only what the app's queries need and add more later. - Never diffing Production's actual schema against the code's write surface — the
missing-field write returns a
CKErrorthat many apps never surface anywhere visible, so the failure mode is only caught by an explicit audit, not by normal testing.
Review Checklist
-
.ckdbis committed under version control; the management token is not. -
exportwas run after a Development-seeding build, not against a partially-provisioned container. -
validate-schemawas run with an explicit--environmentbefore anyimport. - The management token is purged from
cktool's keychain store, even on failure paths. - No tooling assumes
import-schemacan target Production — the Console step is documented as the only path. - A hand-authored or reviewed
.ckdbdeclares only the indexes the app's actual queries need. - Production's exported schema has been diffed against the code's write surface at least once since the last Console promotion.
Related skills
swift-dependency-injection— how CloudKit access is injected and faked, keeping schema concerns out of call sites.swift-testing-baseline— gate live CloudKit/Game Center access behind a test-only suppression seam; constructing a live container or auth handler inside a test blocks the unentitled SwiftPM runner indefinitely (its "unentitled runner" section covers this landmine — this skill's schema is only ever tested against, never through a live container in CI).apple-public-repo-security— why the management token is a stricter secret class than a build-time public identifier.build-time-secret-injection— the general env-file-based secret pattern this workflow's token handling follows.- Official sources: when verifying or updating a factual or version-sensitive claim, read
references/official-docs.md.
Files (apple-dev-skills)
-
references
-
official-docs.md 1.1 KB
Official pages backing this skill's claims; read when verifying or updating a factual or version-sensitive claim. | Page | URL | Backs | |---|---|---| | Deploying an iCloud Container's Schema | https://developer.apple.com/documentation/cloudkit/deploying-an-icloud-container-s-schema | "you can't delete record types or fields that are already in production"; deploy copies indexes, but the no-delete list doesn't cover indexes | | Designing and Creating a CloudKit Database | https://developer.apple.com/documentation/cloudkit/designing-and-creating-a-cloudkit-database | Only additive schema changes; just-in-time schema | | Inspecting and Editing an iCloud Container's Schema | https://developer.apple.com/documentation/cloudkit/inspecting-and-editing-an-icloud-container-s-schema | Viewing/removing indexes; fields in a production schema can't be deleted | | Automate CloudKit tests with cktool and declarative schema (WWDC21) | https://developer.apple.com/videos/play/wwdc2021/10118/ | Management tokens; export/import; "it is possible to add and remove indexes in production" (overrides the add-only index claim elsewhere in this skill) |
-
-
scripts
-
ck-schema-dev.sh 1.7 KB
#!/usr/bin/env bash # # ck-schema-dev.sh — export, validate, and deploy the CloudKit Development # schema. See ../SKILL.md for the full workflow, live-run gotchas, and the # Production-promotion safety gate (Console-only, user-owned) — this script # never touches Production. # # Usage: scripts/ck-schema-dev.sh # Requires secrets/.env (gitignored) exporting: # CK_MANAGEMENT_TOKEN, CK_TEAM_ID, CK_CONTAINER_ID # set -euo pipefail # Always clear the token from cktool's keychain store, even on failure. trap 'xcrun cktool remove-token --type management --force' EXIT # Load credentials for this shell session only. set -a; source secrets/.env; set +a # 1. Authenticate cktool for this session (positional arg — see gotcha 1 in SKILL.md). xcrun cktool save-token --type management --force "$CK_MANAGEMENT_TOKEN" # 2. Export the live Development schema to the committed source-of-truth file. # Seed step first: run a debug build once so the app's JIT schema provisions # the Development container, THEN export. xcrun cktool export-schema \ --team-id "$CK_TEAM_ID" --container-id "$CK_CONTAINER_ID" \ --environment development > cloudkit/myapp.ckdb # 3. Pre-flight: validate the committed .ckdb against the live container before importing. xcrun cktool validate-schema \ --team-id "$CK_TEAM_ID" --container-id "$CK_CONTAINER_ID" \ --environment development --file cloudkit/myapp.ckdb # 4. Deploy to Development — freely runnable and reversible. xcrun cktool import-schema \ --team-id "$CK_TEAM_ID" --container-id "$CK_CONTAINER_ID" \ --environment development --file cloudkit/myapp.ckdb # 5. Token cleanup happens via the trap above, even if a step failed midway.
-
-
SKILL.md 12 KB
--- name: cloudkit-schema-source-of-truth description: 'Committed `.ckdb` as the CloudKit schema source of truth, with `xcrun cktool` export / validate / import against Development and Production promotion kept as a Console-only gate. Use when a CloudKit-backed persistence layer adds or edits a record type, field, or index; before any Production schema deploy; or when asked "how do I push CloudKit schema to Production", "why can''t cktool deploy to prod", "why is a field silently missing in Production". Does NOT own the Swift persistence seam → swift-dependency-injection, or token storage → apple-public-repo-security.' --- # CloudKit Schema Source of Truth CloudKit has no migration-file system like a SQL database. The schema lives in Apple's CloudKit Dashboard/Console, and `xcrun cktool` (Apple's official CLI, ships with Xcode) can export, validate, and import it — but only against the **Development** environment. Production promotion is a manual, irreversible Console action. This skill makes the Development side of that workflow scriptable and commit-trackable while keeping the Production gate correctly user-owned. ## When to invoke - A persistence change adds, renames, or edits a CloudKit record type, field, or index. - You need to push schema to a container (Development or Production). - Before any Production schema deploy — read the safety gate below first. - Asked why a `.ckdb` file is committed to the repo, or why a field the app writes is missing from Production data. ## Scope Owns: the `.ckdb`-as-source-of-truth workflow, `cktool` invocations against Development, and the Production promotion gate. Does **not** own: the Swift-side persistence/service code that reads and writes CloudKit records → `swift-dependency-injection` for how that seam is injected and faked in tests; secret storage for the management token itself → `apple-public-repo-security` / `build-time-secret-injection`. ## Prerequisites (one-time, user-owned) Two credentials, kept in a gitignored env file (e.g. `secrets/.env`, with a committed `.env.example` template): - **A CloudKit management token** — generated by a human in CloudKit Dashboard → Settings → Tokens → Create Token (Management). This is a privileged credential; treat it like an API key with schema-write access, not like a build-time public identifier. - **The Apple Developer Team ID** (10 characters). The container identifier itself (e.g. `iCloud.com.example.myapp`) is not secret and can be hardcoded in tooling. ## Workflow 1. Authenticate `cktool` for this session (positional arg — see gotcha 1 below). 2. Export the live Development schema to the committed source-of-truth file (seed step first: run a debug build once so the app's JIT schema provisions the Development container, THEN export). 3. Pre-flight: validate the committed `.ckdb` against the live container before importing. 4. Deploy to Development — freely runnable and reversible. 5. Always clear the token from `cktool`'s keychain store when done (via a shell `trap ... EXIT` around steps 1–4, so it's purged even if a step fails midway). Runnable as `${CLAUDE_SKILL_DIR}/scripts/ck-schema-dev.sh` (the `scripts/` folder next to this SKILL.md) — run it by that path or copy it into your repo's `scripts/`; see that file for the exact `cktool` invocations and flags. ## Inputs / outputs - **Input**: the credentials above, plus either the live Development container (`export`) or the committed `cloudkit/<app>.ckdb` (`validate` / `import`). - **Output**: `export` overwrites `cloudkit/<app>.ckdb` — review the diff, then commit it as the schema source of truth. `import` mutates the named container's live schema. - `.ckdb` files are **not secrets** — they contain schema definitions only, no data or tokens — so they're committed like any other source file, distinct from the token itself. ## Safety gate — Production promotion is user-owned, Console-only, irreversible **`cktool` cannot push schema to Production.** `import-schema --environment production` rejects with an "endpoint not applicable in this environment" style error, and there is no promote subcommand. The Development → Production promotion happens **only** in the CloudKit Console: 1. Bring Development fully in sync first (`import-schema --environment development` above). 2. Console → your container → environment **Development** → Schema → **"Deploy Schema Changes to Production…"** → review the generated field/index diff → confirm the deploy. CloudKit Production record types and fields are **add-only** by Apple's own rule — once deployed they can't be deleted or renamed, only added to. Indexes are different: they can be added and removed in Production (WWDC21 "Automate CloudKit tests with cktool and declarative schema"). Restricting the promotion path to the Console keeps it naturally user-owned: automation prepares and validates the `.ckdb` and the Development deploy; a human clicks the actual Production button. `export` / `validate` / `import --environment development` are all reversible and safe to run repeatedly without asking anyone. ## Live-run gotchas 1. **`save-token` takes the token as a positional argument, not piped stdin.** Non-interactive stdin piping fails with `Error: Interaction was required while running in non-interactive mode (CKTOOL_NO_PROMPT=1 or not running in an interactive terminal).` (verified locally, cktool 1.0.23001). Brief command-line argv exposure of the token is the tradeoff; purge it from the keychain store immediately after (see the `trap` note above). 2. **`validate-schema` requires `--environment` explicitly** — omitting it is a hard error, not a default. 3. **`import-schema` only ever targets Development.** Don't assume a script that "runs `import-schema --environment production`" has ever actually been exercised — smoke-test any such tooling against real credentials before trusting it; a plausible-looking Production import path that was never live-tested can sit broken for a long time undetected. 4. **Just-in-time (JIT) schema exists only in Development.** A debug build auto-creates record types and fields the first time it writes them, in Development only — Production never does this. Corollary: any field the app code writes that was never JIT-seeded in Development *before* the last Console promotion is **missing in Production**, and the server returns a `CKError` for the unknown field when the app tries to save it — this only *looks* silent if the app's own save-completion handling swallows or ignores that error instead of surfacing it. **Audit method**: `export-schema --environment production` to a scratch file and diff its field set against every field the code actually writes, and confirm every CloudKit save call actually surfaces its error instead of discarding it. 5. **JIT marks every field it creates `QUERYABLE SEARCHABLE SORTABLE`.** A hand-authored `.ckdb` should declare the **minimal** index set the app's actual queries need instead (e.g. only the one field a specific equality query filters on, as `QUERYABLE`) — every index adds query/storage cost and noise to the `.ckdb` diff, so start minimal and add indexes as real queries need them. 6. **`import-schema` is a declarative import**, so a `.ckdb` can be hand-authored from scratch — no Dashboard clicking, no live seed build required. Use one `export`'s output as the syntax template (it includes the system `"___*"` fields and the `GRANT` block a hand-written file also needs). For other `cktool` subcommands/flags not required for the core loop above (e.g. `import-schema --validate`, `export-schema --output-file <path>`), run `xcrun cktool help` / `xcrun cktool <subcommand> --help` — the offline, primary source, more current than any WWDC talk. One flag worth knowing without opening a shell: `reset-schema` resets a container's Development environment to match Production **and deletes all Development data** — a "start clean and re-seed" tool, not part of the routine loop. ## Idempotency - `export`: re-running always overwrites `cloudkit/<app>.ckdb` with the current Development schema — treat the file as generated + reviewed, not hand-edited, whenever a live export is the intended source. - `import`: CloudKit's import is declarative — re-applying the same unchanged `.ckdb` is a no-op. ## Rationale Treating one `.ckdb` per app as the schema source of truth gives CloudKit the same review-before-merge discipline a SQL migration file gets, despite CloudKit having no native migration mechanism. Restricting the token to Development-only tooling, and Production to a Console click, matches Apple's own irreversibility constraint (add-only fields) to a correspondingly irreversible, deliberately manual approval step. ## Deviation considerations - **A container with no meaningful schema evolution** (fixed at launch, never touched again): a single manual export is enough; the ongoing export/validate/import loop isn't worth automating for a container that never changes. - **Multiple apps sharing one CloudKit container**: keep one `.ckdb` per container (not per app) and make the ownership of shared record types explicit in its surrounding docs, so two apps don't independently "fix" the same field in diverging ways. ## Common Mistakes 1. **Assuming `import-schema --environment production` works** because it's syntactically accepted-looking — it is Development-only; Production is Console-only. 2. **Skipping the Development JIT-seed step before an export** — the export then reflects an incomplete schema, and the gap resurfaces later as a Production write returning `CKError` for the unknown field, which looks like a silent failure only if that error is discarded. 3. **Leaving the management token in `cktool`'s keychain store** after a session — purge it even on script failure via a `trap`. 4. **Hand-editing `.ckdb` opportunistically** without re-validating against the live Development container before importing. 5. **Over-indexing a hand-authored `.ckdb`** (marking every field `QUERYABLE SEARCHABLE SORTABLE` out of caution) — every extra index adds query/storage cost and noise to the `.ckdb` diff; declare only what the app's queries need and add more later. 6. **Never diffing Production's actual schema against the code's write surface** — the missing-field write returns a `CKError` that many apps never surface anywhere visible, so the failure mode is only caught by an explicit audit, not by normal testing. ## Review Checklist - [ ] `.ckdb` is committed under version control; the management token is not. - [ ] `export` was run after a Development-seeding build, not against a partially-provisioned container. - [ ] `validate-schema` was run with an explicit `--environment` before any `import`. - [ ] The management token is purged from `cktool`'s keychain store, even on failure paths. - [ ] No tooling assumes `import-schema` can target Production — the Console step is documented as the only path. - [ ] A hand-authored or reviewed `.ckdb` declares only the indexes the app's actual queries need. - [ ] Production's exported schema has been diffed against the code's write surface at least once since the last Console promotion. ## Related skills - `swift-dependency-injection` — how CloudKit access is injected and faked, keeping schema concerns out of call sites. - `swift-testing-baseline` — gate live CloudKit/Game Center access behind a test-only suppression seam; constructing a live container or auth handler inside a test blocks the unentitled SwiftPM runner indefinitely (its "unentitled runner" section covers this landmine — this skill's schema is only ever tested against, never through a live container in CI). - `apple-public-repo-security` — why the management token is a stricter secret class than a build-time public identifier. - `build-time-secret-injection` — the general env-file-based secret pattern this workflow's token handling follows. - Official sources: when verifying or updating a factual or version-sensitive claim, read `references/official-docs.md`.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.