
Cloudkit Code Review
- 108 installs
- 74 repo stars
- Updated July 21, 2026
- existential-birds/beagle
Helps with ai & agent building tasks.
About
cloudkit-code-review is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- cloudkit-code-review
- AI & Agent Building
- AI-coding skill
Cloudkit Code Review by the numbers
- 108 all-time installs (skills.sh)
- Ranked #4,093 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/existential-birds/beagle --skill cloudkit-code-reviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 108 |
|---|---|
| repo stars | ★ 74 |
| Last updated | July 21, 2026 |
| Repository | existential-birds/beagle ↗ |
What it does
Helps with ai & agent building tasks.
Files
CloudKit Code Review
Quick Reference
| Issue Type | Reference |
|---|---|
| CKContainer, databases, zones, entitlements | references/container-setup.md |
| CKRecord, references, assets, batch operations | references/records.md |
| CKSubscription, push notifications, silent sync | references/subscriptions.md |
| CKShare, participants, permissions, acceptance | references/sharing.md |
Review Checklist
- [ ] Account status checked before private/shared database operations
- [ ] Custom zones used (not default zone) for production data
- [ ] All CloudKit errors handled with
retryAfterSecondsrespected - [ ]
serverRecordChangedconflicts handled with proper merge logic - [ ]
CKErrorPartialFailureparsed for individual record errors - [ ] Batch operations used (
CKModifyRecordsOperation) not individual saves - [ ] Large binary data stored as
CKAsset(records have 1MB limit) - [ ] Record keys type-safe (enums) not string literals
- [ ] UI updates dispatched to main thread from callbacks
- [ ]
CKAccountChangedNotificationobserved for account switches - [ ] Subscriptions have unique IDs to prevent duplicates
- [ ] CKShare uses custom zone (sharing requires custom zones)
When to Load References
- Reviewing container/database setup or zones -> container-setup.md
- Reviewing record CRUD or relationships -> records.md
- Reviewing push notifications or sync triggers -> subscriptions.md
- Reviewing sharing or collaboration features -> sharing.md
Output Format
Report issues using: [FILE:LINE] ISSUE_TITLE
Examples:
[AppDelegate.swift:24] CKContainer not in custom zone[SyncManager.swift:156] Unhandled CKErrorPartialFailure[DataStore.swift:89] Missing retryAfterSeconds backoff
Review Questions
1. What happens when the user is signed out of iCloud? 2. Does error handling respect rate limiting (retryAfterSeconds)? 3. Are conflicts resolved or does data get overwritten silently? 4. Is the schema deployed to production before App Store release? 5. Are shared records in custom zones (required for CKShare)?
Hard gates (before reporting)
Complete in order for each finding you intend to report. Do not advance until the pass condition is satisfied.
1. Location artifact — The finding includes [FILE:LINE] (or a line range) copied from the current file contents; the path resolves in this repo. 2. Scope read — You read the full surrounding unit: the type or function that owns the CloudKit work (for example the CKOperation subclass usage, completion handler chain, or CKRecord lifecycle), not only a diff hunk or isolated snippet. 3. CloudKit or deployment claim (only if the finding depends on container identifiers, public vs private database choice, custom zone requirement, iCloud account state, entitlements, or production schema) — You name one concrete artifact you inspected (for example com.apple.developer.icloud-container-environment or container ID in the entitlements file, CKContainer.default() vs custom identifier in source, Info.plist / target capability, or evidence that schema is deployed) or you downgrade the item to an open question in Review Questions. 4. Protocol — Pre-report steps in review-verification-protocol are satisfied for this item (no finding if they are not).
Use the issue format [FILE:LINE] ISSUE_TITLE for each reported finding. Hard gate 4 is the full pre-report checklist for this skill’s review type.
CloudKit Container Setup
Container Architecture
// Default container (matches app's bundle identifier)
let container = CKContainer.default()
// Custom container (explicit identifier - recommended)
let container = CKContainer(identifier: "iCloud.com.company.appname")Container identifiers cannot be deleted once created - verify naming before creation.
Database Types
| Database | Access | Custom Zones | Use Case |
|---|---|---|---|
| Private | User-only (requires iCloud) | Yes | Personal data |
| Public | Read: anyone; Write: signed-in | No (default only) | App-wide content |
| Shared | Invited users only | Yes (one per sharer) | Collaboration |
let privateDB = container.privateCloudDatabase
let publicDB = container.publicCloudDatabase
let sharedDB = container.sharedCloudDatabaseCustom Zones
Custom zones provide atomic operations, sharing, and change tokens. Required for production apps.
let zoneID = CKRecordZone.ID(zoneName: "MyZone", ownerName: CKCurrentUserDefaultName)
let zone = CKRecordZone(zoneID: zoneID)
let operation = CKModifyRecordZonesOperation(recordZonesToSave: [zone], recordZoneIDsToDelete: nil)
privateDB.add(operation)Critical Anti-Patterns
1. Using Default Zone for Production
// BAD: Default zone lacks atomic operations and sharing
let record = CKRecord(recordType: "Note")
privateDB.save(record) { _, _ in }
// GOOD: Use custom zone
let zoneID = CKRecordZone.ID(zoneName: "NotesZone", ownerName: CKCurrentUserDefaultName)
let recordID = CKRecord.ID(recordName: UUID().uuidString, zoneID: zoneID)
let record = CKRecord(recordType: "Note", recordID: recordID)2. Missing Account Status Check
// BAD: Assumes iCloud is available
func saveUserData() {
container.privateCloudDatabase.save(record) { _, _ in }
}
// GOOD: Check account status first
container.accountStatus { status, error in
guard status == .available else {
// Handle: .noAccount, .restricted, .couldNotDetermine
return
}
self.container.privateCloudDatabase.save(record) { _, _ in }
}3. Not Observing Account Changes
// BAD: Assumes account persists
class DataManager {
let container = CKContainer.default()
}
// GOOD: Observe account changes
NotificationCenter.default.addObserver(
forName: .CKAccountChanged,
object: nil,
queue: .main
) { _ in
// Re-check account status, clear private data cache if user changed
}4. Missing NSPersistentCloudKitContainer Options
// BAD: Missing required options
let container = NSPersistentCloudKitContainer(name: "Model")
container.loadPersistentStores { _, _ in }
// GOOD: Enable required tracking
let description = container.persistentStoreDescriptions.first!
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
containerIdentifier: "iCloud.com.company.app"
)Required Entitlements
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudKit</string>
</array>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.com.company.appname</string>
</array>For production:
<key>com.apple.developer.icloud-container-environment</key>
<string>Production</string>Review Questions
1. Is the container identifier explicitly specified or relying on .default()? 2. Is account status checked before accessing private/shared databases? 3. Are custom zones used for production features? 4. Is CKAccountChanged notification observed? 5. Are entitlements configured for both app and extensions? 6. Is the production environment entitlement set for release builds?
CloudKit Records
CKRecord Basics
Supported field types:
String,NSNumber,Data,Date,CLLocationCKRecord.Reference- links to other recordsCKAsset- binary files (images, audio, documents)- Arrays of any above type (same-type elements only)
Size limits:
| Constraint | Limit |
|---|---|
| Single record (excluding assets) | 1 MB |
| Single asset | 250 MB (native) |
| Batch operations per request | ~400 records |
CKRecord.Reference
// Child points to parent with cascade delete
let parentRef = CKRecord.Reference(recordID: parentRecord.recordID, action: .deleteSelf)
childRecord["parentRef"] = parentRefActions:
.deleteSelf- Child deleted when parent deleted.none- Child becomes orphan when parent deleted
CKAsset
let fileURL = getLocalFileURL()
let asset = CKAsset(fileURL: fileURL)
record["attachment"] = assetAssets stored separately, don't count toward 1MB record limit.
Critical Anti-Patterns
1. Storing Child Arrays in Parent
// BAD: Causes conflict resolution nightmares
let parentRecord = CKRecord(recordType: "Album")
parentRecord["photoIDs"] = photoIDs as CKRecordValue
// GOOD: Child references parent
let photoRecord = CKRecord(recordType: "Photo")
let albumRef = CKRecord.Reference(recordID: albumRecord.recordID, action: .deleteSelf)
photoRecord["album"] = albumRef2. Ignoring Errors
// BAD
database.save(record) { _, error in
self.updateUI() // Ignores error!
}
// GOOD
database.save(record) { _, error in
if let error = error as? CKError {
switch error.code {
case .serverRecordChanged:
self.resolveConflict(error: error)
case .networkUnavailable, .networkFailure:
if let retry = error.userInfo[CKErrorRetryAfterKey] as? Double {
DispatchQueue.main.asyncAfter(deadline: .now() + retry) {
self.retrySave(record)
}
}
default:
self.handleError(error)
}
return
}
DispatchQueue.main.async { self.updateUI() }
}3. String Literals for Keys
// BAD: Typos won't be caught
record["titel"] = title
// GOOD: Type-safe keys
enum RecordKeys: String {
case title, createdAt, category
}
record[RecordKeys.title.rawValue] = title4. Individual Saves Instead of Batch
// BAD: Separate network call for each record
for record in records {
database.save(record) { _, _ in }
}
// GOOD: Single batch operation
let operation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
operation.modifyRecordsResultBlock = { result in }
database.add(operation)5. Exceeding Record Size
// BAD: May exceed 1MB limit
record["imageData"] = largeImageData as CKRecordValue
// GOOD: Use CKAsset for binary data
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("temp.jpg")
try imageData.write(to: tempURL)
record["image"] = CKAsset(fileURL: tempURL)6. UI Updates on Background Thread
// BAD: CloudKit callbacks are on background thread
database.fetch(withRecordID: recordID) { record, error in
self.titleLabel.text = record?["title"] as? String // Crash!
}
// GOOD
database.fetch(withRecordID: recordID) { record, error in
DispatchQueue.main.async {
self.titleLabel.text = record?["title"] as? String
}
}7. Downloading All Fields
// BAD: Downloads everything including large assets
let query = CKQuery(recordType: "Photo", predicate: predicate)
database.perform(query, inZoneWith: nil) { records, error in }
// GOOD: Only fetch needed fields
let operation = CKQueryOperation(query: query)
operation.desiredKeys = ["title", "timestamp"]
database.add(operation)Error Handling Table
| Error Code | Common Mistake | Correct Handling |
|---|---|---|
partialFailure | Treat as complete failure | Parse partialErrorsByItemID |
serverRecordChanged | Retry with client record | Merge using server record from error |
requestRateLimited | Immediate retry | Use retryAfterSeconds |
limitExceeded | Fail operation | Split batch and retry |
quotaExceeded | Silent failure | Alert user |
Review Questions
1. Are custom record IDs used that match local storage identifiers? 2. Is record data under 1MB with large files as CKAssets? 3. Are relationships using back-references (child->parent) not arrays? 4. Is .deleteSelf used appropriately for cascade delete needs? 5. Are all CloudKit callbacks dispatching UI updates to main thread? 6. Is desiredKeys specified to avoid downloading unnecessary data?
CloudKit Sharing
Sharing Models
| Model | Use Case | CKShare Creation |
|---|---|---|
| Record Sharing | Individual records with hierarchy | CKShare(rootRecord: record) |
| Zone Sharing | All records in custom zone | CKShare(recordZoneID: zone.zoneID) |
Permission Levels
| Permission | Description |
|---|---|
.none | No access (default for publicPermission) |
.readOnly | Can read shared records |
.readWrite | Can read and modify shared records |
Database Architecture
Private Database (Owner)
└── Custom Zone (required!)
├── Root Record
├── Child Records (auto-shared)
└── CKShare Record
Shared Database (Participants)
└── [View into owner's private database]Critical Anti-Patterns
1. Using Default Zone for Sharing
// BAD: CKShare cannot be saved in Default Zone
let record = CKRecord(recordType: "Item") // Uses default zone
let share = CKShare(rootRecord: record)
try await privateDatabase.save(share) // ERROR!
// GOOD: Use custom zone
let zoneID = CKRecordZone.ID(zoneName: "SharedItems", ownerName: CKCurrentUserDefaultName)
let recordID = CKRecord.ID(recordName: UUID().uuidString, zoneID: zoneID)
let record = CKRecord(recordType: "Item", recordID: recordID)
let share = CKShare(rootRecord: record)
try await privateDatabase.modifyRecords(saving: [record, share], deleting: [])2. Saving CKShare Without Root Record
// BAD: Even if record exists, must save together
let share = CKShare(rootRecord: existingRecord)
try await privateDatabase.save(share) // ERROR!
// GOOD: Save both together
try await privateDatabase.modifyRecords(saving: [existingRecord, share], deleting: [])3. Creating New Shares for Already-Shared Records
// BAD: Revokes existing share, removes all participants
func shareContact(_ contact: CKRecord) async throws {
let share = CKShare(rootRecord: contact) // Creates NEW share!
try await privateDatabase.modifyRecords(saving: [contact, share], deleting: [])
}
// GOOD: Check for existing share first
func shareContact(_ contact: CKRecord) async throws -> CKShare {
if let existingShareRef = contact.share {
return try await privateDatabase.record(for: existingShareRef.recordID) as! CKShare
}
let share = CKShare(rootRecord: contact)
try await privateDatabase.modifyRecords(saving: [contact, share], deleting: [])
return share
}4. Not Verifying Permissions Before Modification
// BAD: Assumes write access
func updateSharedRecord(_ record: CKRecord) async throws {
record["name"] = "Updated"
try await sharedDatabase.save(record) // Fails if readOnly!
}
// GOOD: Check permission first
func canModify(share: CKShare) -> Bool {
guard let participant = share.currentUserParticipant else { return false }
return participant.permission == .readWrite || share.owner == participant
}5. Missing CKSharingSupported in Info.plist
<!-- Required for share acceptance callbacks -->
<key>CKSharingSupported</key>
<true/>Without this, userDidAcceptCloudKitShareWith is never called.
6. Not Handling Share Acceptance
// BAD: Share links won't work
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// Missing implementation
}
// GOOD
func windowScene(
_ windowScene: UIWindowScene,
userDidAcceptCloudKitShareWith metadata: CKShare.Metadata
) {
let container = CKContainer(identifier: metadata.containerIdentifier)
Task {
do {
try await container.accept(metadata)
} catch {
// Handle error
}
}
}7. Not Setting Share Metadata
// BAD: Email invitations show no context
let share = CKShare(rootRecord: record)
// GOOD: Set title for user-friendly invitations
let share = CKShare(rootRecord: record)
share[CKShare.SystemFieldKey.title] = "Shopping List"
share[CKShare.SystemFieldKey.shareType] = "com.app.shoppinglist"
share[CKShare.SystemFieldKey.thumbnailImageData] = thumbnailData8. Multiple CKShare per Zone
// BAD: Only ONE CKShare allowed per zone
let share1 = CKShare(recordZoneID: zoneID)
try await privateDatabase.save(share1)
let share2 = CKShare(recordZoneID: zoneID) // ERROR on save!
// GOOD: Check for existing zone share
func getOrCreateZoneShare(for zoneID: CKRecordZone.ID) async throws -> CKShare {
let shareID = CKRecord.ID(recordName: CKRecordNameZoneWideShare, zoneID: zoneID)
do {
return try await privateDatabase.record(for: shareID) as! CKShare
} catch {
let share = CKShare(recordZoneID: zoneID)
try await privateDatabase.save(share)
return share
}
}UICloudSharingController Integration
func presentShareController(for share: CKShare, record: CKRecord) {
// Pre-fetch share before presenting
let controller = UICloudSharingController(share: share, container: container)
controller.delegate = self
present(controller, animated: true)
}
// Required delegate methods
extension ViewController: UICloudSharingControllerDelegate {
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
// Handle error
}
func itemTitle(for csc: UICloudSharingController) -> String? {
return "Shared Item"
}
}Review Questions
1. Is CKShare saved to a custom zone (not Default Zone)? 2. Is root record saved together with CKShare in same operation? 3. Does code check for existing shares before creating new ones? 4. Is CKSharingSupported enabled in Info.plist? 5. Is userDidAcceptCloudKitShareWith implemented? 6. Are shared records accessed from sharedDatabase (not privateDatabase)? 7. Does code verify permissions before attempting modifications? 8. Is share metadata (title, type) set for user-friendly invitations?
CloudKit Subscriptions
Subscription Types
| Type | Use Case | Database Support |
|---|---|---|
| CKQuerySubscription | Records matching predicate | Public, Private (default zone) |
| CKRecordZoneSubscription | All changes in custom zone | Private only |
| CKDatabaseSubscription | All changes across database | Private, Shared |
Recommendation: Start with CKDatabaseSubscription unless only using default zone.
Notification Configuration
let info = CKSubscription.NotificationInfo()
// Visible notification
info.alertBody = "New record available"
info.soundName = "default"
info.shouldBadge = true
// Silent notification (background sync)
info.shouldSendContentAvailable = true
// Leave alertBody, soundName, shouldBadge unsetCritical Anti-Patterns
1. Creating Duplicate Subscriptions
// BAD: Creates duplicate on every app launch
func application(_ application: UIApplication, didFinishLaunchingWithOptions...) {
let subscription = CKQuerySubscription(recordType: "Item", predicate: predicate, options: .firesOnRecordCreation)
database.save(subscription) { _, _ in }
}
// GOOD: Check before creating, use consistent ID
let subscriptionID = "item-creation-subscription"
database.fetch(withSubscriptionID: subscriptionID) { subscription, error in
if subscription == nil {
let newSubscription = CKQuerySubscription(
recordType: "Item",
predicate: predicate,
subscriptionID: subscriptionID,
options: .firesOnRecordCreation
)
let info = CKSubscription.NotificationInfo()
info.shouldSendContentAvailable = true
newSubscription.notificationInfo = info
database.save(newSubscription) { _, _ in }
}
}2. Missing NotificationInfo
// BAD: Subscription will fail to save
let subscription = CKQuerySubscription(recordType: "Item", predicate: predicate, options: .firesOnRecordCreation)
database.save(subscription) { _, error in } // Error!
// GOOD: Always configure notificationInfo
let info = CKSubscription.NotificationInfo()
info.shouldSendContentAvailable = true
subscription.notificationInfo = info3. Wrong Subscription Type for Shared Database
// BAD: CKQuerySubscription doesn't work with shared database
let sharedDB = CKContainer.default().sharedCloudDatabase
let subscription = CKQuerySubscription(recordType: "SharedItem", predicate: predicate, options: .firesOnRecordCreation)
sharedDB.save(subscription) { _, error in } // Error!
// GOOD: Use CKDatabaseSubscription for shared database
let subscription = CKDatabaseSubscription(subscriptionID: "shared-db-subscription")
let info = CKSubscription.NotificationInfo()
info.shouldSendContentAvailable = true
subscription.notificationInfo = info
sharedDB.save(subscription) { _, _ in }4. Relying Solely on Push for Sync
// BAD: Only syncing when push arrives
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
syncData() // Only sync trigger
}
// GOOD: Multiple sync triggers
func applicationDidBecomeActive(_ application: UIApplication) {
syncData() // On app launch
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
syncData() // On notification
}
// Also implement background fetch5. Not Indexing Predicate Fields
// BAD: Field not indexed in CloudKit Dashboard
let predicate = NSPredicate(format: "category == %@", "news")
// Error: CKError.invalidArguments when saving subscription
// FIX: Enable "Query" indexing for field in CloudKit DashboardRegistration Flow
// 1. Request authorization
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
// 2. Register for remote notifications
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
// 3. Create subscription after registration
subscriptionManager.ensureSubscriptionExists()Subscription Manager Pattern
class SubscriptionManager {
private let subscriptionKey = "cloudkit.subscription.created"
func ensureSubscriptionExists() {
guard !UserDefaults.standard.bool(forKey: subscriptionKey) else { return }
let subscription = CKDatabaseSubscription(subscriptionID: "all-changes")
let info = CKSubscription.NotificationInfo()
info.shouldSendContentAvailable = true
subscription.notificationInfo = info
let operation = CKModifySubscriptionsOperation(
subscriptionsToSave: [subscription],
subscriptionIDsToDelete: nil
)
operation.modifySubscriptionsCompletionBlock = { saved, _, error in
if error == nil {
UserDefaults.standard.set(true, forKey: self.subscriptionKey)
}
}
database.add(operation)
}
}Review Questions
1. Is a specific subscriptionID used to prevent duplicates? 2. Is notificationInfo properly configured before saving? 3. Is the correct subscription type used for the database (shared needs CKDatabaseSubscription)? 4. Are predicate fields indexed in CloudKit Dashboard? 5. Is shouldSendContentAvailable set for silent notifications (without alertBody)? 6. Does the app handle coalesced notifications (not 1:1 with changes)? 7. Is there fallback sync logic for when notifications don't arrive? 8. Is the schema deployed to production before App Store release?