NoteSync — Interview Talking Points

The 30-second pitch

“NoteSync is a collaborative notes app on iOS and macOS where users can share individual notes or whole folders with other iCloud users. Sharing uses CloudKit’s CKShare model: I generate a per-share zone, drop the shared records into it, and produce a share URL. The recipient accepts on their device and from that point both clients read and write to the shared zone with real-time deltas via silent push subscriptions. The interesting work was conflict resolution — when two users edit the same note simultaneously, CloudKit returns a server-record-changed error with the divergent versions. I built a three-way merge using the common ancestor that combines non-overlapping line-level edits automatically and surfaces overlapping conflicts to the user for manual resolution.”

The 3-minute deep dive (sharing topology)

“The hardest mental model in CloudKit sharing is that sharing is zone-scoped, not record-scoped. You can’t just share one record while keeping the rest private — the share gives access to the zone containing that record. So when the user says ‘share this note,’ my code first moves the note into a new dedicated zone, then creates a CKShare rooted on it.

That has a subtle consequence: every share creates a new zone. Users with 100 shared notes have 100 shared zones — but each zone is tiny, so it works. The alternative — one big ‘Shared’ zone — would mean any share grants access to everything.

For folders, the same logic: I move all the folder’s notes plus the folder record itself into one new zone, share at the folder level, and the recipient gets a coherent unit.

Sync is two streams in parallel: CKFetchDatabaseChangesOperation against .privateCloudDatabase for user-owned content, and the same against .sharedCloudDatabase for content shared with them. Each database has its own change token. Each zone within each database has its own change token. So my engine persists a tree of tokens and walks it.

Silent push subscriptions wake the app when CloudKit has new data. The didReceiveRemoteNotification handler kicks off a sync. From device-write to remote-device-display is around 5 seconds in good network conditions.

The piece I’m proudest of is the conflict resolution. When CKModifyRecordsOperation fails with .serverRecordChanged, the error carries the server’s current record, my client’s pending version, and the ancestor I had locally. I run a line-level diff from ancestor to local and from ancestor to server. If the changed line ranges don’t overlap, I apply both diffs and resave. If they overlap, I store both versions as a ConflictRecord and the UI shows the user ‘resolve 1 conflict.’ Lossless by construction.“

12 interview questions

1. “Why CloudKit and not Firebase / Supabase / your own backend?”

Three reasons. (1) The user’s data lives in their iCloud — strong privacy story, no third-party trust. (2) No server infrastructure to run, scale, or pay for. (3) Sign in is free — iCloud account handles auth. The tradeoffs: no web access (CloudKit JS is a separate world we’re not entering), Apple-platform-only, and CKShare is finicky to learn. For a notes app where privacy is core to the value prop, CloudKit is the right choice.

2. “Walk me through how sharing works.”

User taps Share on a note. My code creates a new CKRecordZone in their private DB. Moves the note’s CKRecord into that zone. Creates a CKShare(rootRecord:) pointing at the note. Saves both. I then present UICloudSharingController with the CKShare; it generates a share.url and lets the user pick a delivery method. The recipient receives the URL via iMessage or email, taps it, iOS opens NoteSync, the system calls application(_:userDidAcceptCloudKitShareWith:), and my handler runs CKAcceptSharesOperation. From that point the shared zone appears in the recipient’s .sharedCloudDatabase and syncs normally.

3. “Why a zone per share?”

CloudKit sharing is zone-scoped. The share grants access to a whole zone. If I put all shared content in one zone, any share would expose everything. Per-share zones isolate each grant. The cost is more zones to track, but each is small and CloudKit doesn’t limit zone count meaningfully.

4. “What’s the limit on CloudKit zones?”

There’s no explicit per-user zone limit in current CloudKit docs; informal community findings suggest you can comfortably get to thousands per user before things get sluggish on bulk fetches. We’re nowhere near that — a heavy user might have 50 zones (one per shared item). For an app expecting users to share 10,000 items, you’d want a different model — maybe a single shared zone with record-level ACLs handled via your own server.

5. “Tell me about the conflict resolution.”

CloudKit returns serverRecordChanged when your save assumes a stale server version. The error carries serverRecord, clientRecord, ancestorRecord (the version you saw last). I run a line-level diff from ancestor to local and ancestor to server. If changed ranges don’t overlap, I produce a merged record applying both. If they overlap, I treat it as a true conflict — store both versions in a ConflictRecord table and surface to the user. The user picks one, merges manually, or accepts an auto-suggestion. Lossless.

6. “What about offline edits?”

Local writes go to GRDB immediately. The sync engine queues them as pending CKModifyRecordsOperations. When connectivity returns, they flush. Conflict path triggers if the server changed in the interim. Critical detail: writes are persisted as queue entries in the database, not held in memory — so app kills don’t lose them.

7. “Why GRDB and not SwiftData or Core Data?”

For conflict resolution I need deterministic control over the schema, change tokens, and the diff between local and server records. GRDB’s SQLite-backed model gives that with zero ambiguity. SwiftData and Core Data both add abstractions (lazy faulting, automatic save merging, change-set generation) that interfere with the explicit token-based reconciliation I need. For a simpler app I’d happily use SwiftData.

8. “How do AppIntents work cross-platform?”

The same intent compiled into both iOS and macOS targets. The Shortcuts app on each platform discovers them. Same Swift code, same parameter UI. The intent runs in either the AppIntents Extension (no app launch, used for Siri) or in-app (for actions requiring UI). For NoteSync, CreateNoteIntent and AppendToNoteIntent run extension-only; OpenNote requires the app to come forward.

9. “What’s your CloudKit subscription strategy?”

One CKDatabaseSubscription per database with silent push. On receipt, app triggers a sync. Cheap to set up, low push volume, and CloudKit handles delivery. I don’t use zone-level subscriptions because they’d multiply by zone count and offer no benefit at our scale.

10. “How would you add comments?”

Comments are first-class records linked to a note. Add Comment to the schema. When sharing a note, the share zone contains the note + all its comments. Permissions: writable for collaborators, immutable after author posts (or editable for 5 min). Sync the same way as notes. The UI shows them as a thread below the note body. About 3 days of work.

11. “How would you add a web view?”

CloudKit JS — Apple’s JS SDK for CloudKit. Runs in a browser, authenticates against the user’s iCloud, talks to the same container. The schema’s the same; just a different client. The catch is that CKShare flows from CloudKit JS are limited — sharing UI mostly happens on Apple devices. Acceptable for a read-mostly web view.

12. “Walk me through a bug you debugged.”

Two users were sharing a folder; one user kept seeing a ‘duplicate’ note: the same title, different bodies. Took two days. Turned out the silent push was waking the app while a write was in-flight, my sync ran concurrently, both wrote slightly-different records with the same UUID, and CloudKit accepted both because they had different recordChangeTags. Fix: the sync engine takes a Task-local lock; concurrent sync calls wait. Lesson: assume CloudKit will surprise you with concurrency edge cases, and serialize where it matters.

Red-flag answers

If asked “why not real-time presence (typing indicators),” don’t say “I didn’t get to it.” Say: “Out of scope for v1. CloudKit isn’t a real-time channel — its latency floor is in the seconds, not milliseconds. For presence I’d add a thin WebSocket service alongside CloudKit, used only for ephemeral state, with CloudKit still owning persistence.”

If asked “what about end-to-end encryption,” don’t say “CloudKit does it.” Say: “CloudKit encrypts at rest and in transit. For true end-to-end encryption where Apple can’t read the content, I’d layer a client-side encryption scheme on top — but key distribution across shared zones is its own multi-week problem. v2.”


Next: Capstone 5 — DevPortfolio.