diff --git a/DevAccount-Migration.md b/DevAccount-Migration.md
new file mode 100644
index 0000000..871d724
--- /dev/null
+++ b/DevAccount-Migration.md
@@ -0,0 +1,359 @@
+# Developer Account Migration Guide
+
+This document provides a comprehensive guide for migrating the BusinessCard app from the personal `mbrucedogs` developer account to a new Apple Developer account.
+
+## Current State Analysis
+
+### Account Information
+- **Current Team ID**: `6R7KLBPBLZ`
+- **Current Bundle ID Prefix**: `com.mbrucedogs`
+
+### Targets and Bundle Identifiers
+
+| Target | Current Bundle ID |
+|--------|------------------|
+| iOS App | `com.mbrucedogs.BusinessCard` |
+| Watch App | `com.mbrucedogs.BusinessCard.watchkitapp` |
+| Tests | `com.mbrucedogs.BusinessCardTests` |
+| UI Tests | `com.mbrucedogs.BusinessCardUITests` |
+| App Clip (planned) | `com.mbrucedogs.BusinessCard.Clip` |
+
+### Entitlements and Services
+
+| Service | Current Identifier | Migration Impact |
+|---------|-------------------|------------------|
+| CloudKit Container | `iCloud.com.mbrucedogs.BusinessCard` | **HIGH** - Data loss risk |
+| App Group | `group.com.mbrucedogs.BusinessCard` | Medium - Local data only |
+| Push Notifications | `aps-environment: development` | Low - Re-register |
+| Associated Domains (planned) | `appclips:cards.example.com` | Low - Domain-based |
+
+---
+
+## Migration Complexity Rating: MODERATE-HIGH
+
+The main complexity comes from CloudKit. If users have data stored in CloudKit, changing the container identifier means:
+- Users lose access to their synced data
+- No automatic migration path exists
+- Manual data export/import would be required
+
+---
+
+## Pre-Migration Checklist
+
+### Before You Start
+
+- [ ] Create the new Apple Developer Account
+- [ ] Note the new Team ID (found in Membership section of developer portal)
+- [ ] Decide on new bundle ID prefix (e.g., `com.newcompany`)
+- [ ] Plan for CloudKit data migration (if app is in production)
+- [ ] If app is NOT yet released, migration is straightforward
+
+---
+
+## Migration Steps
+
+### Step 1: Update Team ID in Xcode
+
+**Files affected**: `project.pbxproj`
+
+1. Open project in Xcode
+2. Select project in navigator
+3. For each target, go to **Signing & Capabilities**
+4. Change **Team** dropdown to new account
+5. Xcode will update `DEVELOPMENT_TEAM` in 9 places
+
+**Or manually replace in** `BusinessCard.xcodeproj/project.pbxproj`:
+```
+Find: DEVELOPMENT_TEAM = 6R7KLBPBLZ;
+Replace: DEVELOPMENT_TEAM = NEW_TEAM_ID;
+```
+
+---
+
+### Step 2: Update Bundle Identifiers
+
+**Files affected**: `project.pbxproj`
+
+| Old | New |
+|-----|-----|
+| `com.mbrucedogs.BusinessCard` | `com.newcompany.BusinessCard` |
+| `com.mbrucedogs.BusinessCard.watchkitapp` | `com.newcompany.BusinessCard.watchkitapp` |
+| `com.mbrucedogs.BusinessCardTests` | `com.newcompany.BusinessCardTests` |
+| `com.mbrucedogs.BusinessCardUITests` | `com.newcompany.BusinessCardUITests` |
+
+1. In Xcode, select each target
+2. Update **Bundle Identifier** in General tab
+3. Verify watch app bundle ID is prefixed with iOS app bundle ID
+
+---
+
+### Step 3: Update CloudKit Container
+
+**Files affected**: `BusinessCard/BusinessCard.entitlements`
+
+```xml
+
+com.apple.developer.icloud-container-identifiers
+
+ iCloud.com.mbrucedogs.BusinessCard
+
+
+
+com.apple.developer.icloud-container-identifiers
+
+ iCloud.com.newcompany.BusinessCard
+
+```
+
+**CRITICAL**: This creates a NEW, EMPTY CloudKit container. See Data Migration section below.
+
+---
+
+### Step 4: Update App Group
+
+**Files affected**: `BusinessCard/BusinessCard.entitlements`
+
+```xml
+
+com.apple.security.application-groups
+
+ group.com.mbrucedogs.BusinessCard
+
+
+
+com.apple.security.application-groups
+
+ group.com.newcompany.BusinessCard
+
+```
+
+**Impact**: App Group is used for sharing data between the iOS app and extensions (not watch). Local data in the old group will be inaccessible.
+
+---
+
+### Step 5: Register New Identifiers in Developer Portal
+
+In the new Apple Developer account portal:
+
+1. **Identifiers > App IDs**
+ - Register `com.newcompany.BusinessCard`
+ - Register `com.newcompany.BusinessCard.watchkitapp`
+ - Register `com.newcompany.BusinessCard.Clip` (for App Clip)
+
+2. **Identifiers > iCloud Containers**
+ - Create `iCloud.com.newcompany.BusinessCard`
+
+3. **Identifiers > App Groups**
+ - Create `group.com.newcompany.BusinessCard`
+
+4. **Certificates, Identifiers & Profiles**
+ - Create new provisioning profiles for all targets
+ - Xcode can auto-manage this if "Automatically manage signing" is enabled
+
+---
+
+### Step 6: Update Code References
+
+Search and replace in codebase for hardcoded identifiers.
+
+**Files to check** (based on grep results):
+- `BusinessCard/BusinessCardApp.swift`
+- `README.md`
+- `ai_implementation.md`
+- Any service files referencing container IDs
+
+**Search for**:
+```
+com.mbrucedogs
+iCloud.com.mbrucedogs
+group.com.mbrucedogs
+```
+
+**Example in SharedCardCloudKitService** (from OPUS plan):
+```swift
+// Update default parameter
+init(containerID: String = "iCloud.com.newcompany.BusinessCard", ...)
+```
+
+---
+
+### Step 7: Update Associated Domains (If App Clip Implemented)
+
+**Files affected**: Both iOS app and App Clip entitlements
+
+The domain itself (`cards.example.com`) doesn't change, but the server's `apple-app-site-association` file needs the new Team ID:
+
+```json
+{
+ "appclips": {
+ "apps": ["NEW_TEAM_ID.com.newcompany.BusinessCard.Clip"]
+ },
+ "applinks": {
+ "apps": [],
+ "details": [{
+ "appID": "NEW_TEAM_ID.com.newcompany.BusinessCard",
+ "paths": ["/appclip/*"]
+ }]
+ }
+}
+```
+
+---
+
+## CloudKit Data Migration
+
+### Scenario A: App Not Yet Released
+
+**Effort**: None
+
+If the app hasn't been released to users, there's no data to migrate. Simply use the new container.
+
+### Scenario B: App Released, Minimal Users
+
+**Effort**: Low
+
+1. Communicate to users that data will reset
+2. Provide in-app export feature before migration (export to vCard files)
+3. After update, users re-import their cards
+
+### Scenario C: App Released, Significant Users
+
+**Effort**: HIGH
+
+CloudKit doesn't support container migration. Options:
+
+#### Option 1: Dual Container (Recommended)
+
+Keep access to both containers temporarily:
+
+```xml
+com.apple.developer.icloud-container-identifiers
+
+ iCloud.com.mbrucedogs.BusinessCard
+ iCloud.com.newcompany.BusinessCard
+
+```
+
+Then implement in-app migration:
+1. On launch, check old container for data
+2. Copy records to new container
+3. Delete from old container
+4. Remove old container access in future update
+
+**Complexity**: Requires both accounts to grant container access, or the old account must add the new Team ID to the old container's permissions.
+
+#### Option 2: Backend Migration Service
+
+1. Build a server that can access both containers
+2. Migrate data server-side
+3. Coordinate with app update
+
+**Complexity**: Requires server infrastructure and CloudKit server-to-server keys.
+
+#### Option 3: User-Initiated Export/Import
+
+1. Add "Export All Cards" feature (before migration)
+2. Export to files (vCard, JSON, or iCloud Drive)
+3. Release update with new container
+4. Add "Import Cards" feature
+5. Users manually export/import
+
+**Complexity**: Lowest technical effort, but poor user experience.
+
+---
+
+## SwiftData Considerations
+
+SwiftData with CloudKit uses the CloudKit container for sync. When the container changes:
+
+- **Private database records**: Lost (unless migrated)
+- **Local SwiftData store**: Remains on device but stops syncing
+- **New installs**: Start fresh with empty database
+
+If using SwiftData's automatic CloudKit sync, consider:
+1. Disable CloudKit sync temporarily during migration
+2. Keep local data intact
+3. Re-enable sync with new container
+4. Local data will upload to new container
+
+---
+
+## Watch App Considerations
+
+The watch app uses WatchConnectivity (not App Groups) for data sync, so:
+- **Bundle ID change**: Required (must match iOS app prefix)
+- **Data sync**: Unaffected (syncs from iOS app)
+- **Re-pairing**: May need to reinstall watch app after bundle ID change
+
+---
+
+## App Store Considerations
+
+### Same Account, New Bundle ID
+- This is a NEW app listing
+- Lose reviews, ratings, download history
+- Can reference old app in description
+
+### App Transfer (Same Bundle ID)
+- If keeping the same bundle ID but transferring ownership
+- Use App Store Connect's "Transfer App" feature
+- Preserves reviews, ratings, users
+- **Requires**: Both accounts in good standing, no active TestFlight, no pending updates
+
+### Recommendation
+
+If possible, use **App Transfer** to maintain continuity. This requires:
+1. Old account initiates transfer
+2. New account accepts transfer
+3. Bundle ID stays the same
+4. Only Team ID changes in project
+5. CloudKit container permissions may need updates
+
+---
+
+## Complete File Change Summary
+
+| File | Changes Required |
+|------|------------------|
+| `project.pbxproj` | Team ID (9 places), Bundle IDs (6 places) |
+| `BusinessCard.entitlements` | CloudKit container, App Group |
+| `BusinessCardWatch.entitlements` | None (currently empty) |
+| `SharedCardCloudKitService.swift` | Container ID parameter |
+| `README.md` | Update any bundle ID references |
+| `ai_implementation.md` | Update any bundle ID references |
+| Server AASA file | Update Team ID |
+
+---
+
+## Testing After Migration
+
+1. **Clean build**: Delete derived data, clean build folder
+2. **Fresh install**: Delete app from device, reinstall
+3. **CloudKit Dashboard**: Verify new container is receiving data
+4. **Watch app**: Verify watch installs and syncs correctly
+5. **Push notifications**: Verify registration with new bundle ID
+6. **App Clip**: Test invocation URL with new Team ID
+
+---
+
+## Timeline Estimate
+
+| Phase | Duration |
+|-------|----------|
+| Preparation (new account setup) | 1-2 days |
+| Code changes | 1-2 hours |
+| Testing | 1-2 days |
+| CloudKit data migration (if needed) | 1-2 weeks |
+| App Store transition | 1-3 days |
+
+---
+
+## Recommendation
+
+**If the app is not yet released**: Migrate now while it's simple.
+
+**If the app is released**: Consider using App Transfer to keep the same bundle ID and minimize disruption. You'll still need to update the Team ID and potentially CloudKit container access.
+
+---
+
+*Last updated: January 10, 2026*
diff --git a/OPUS-Plan-AppClip.md b/OPUS-Plan-AppClip.md
new file mode 100644
index 0000000..09fbac2
--- /dev/null
+++ b/OPUS-Plan-AppClip.md
@@ -0,0 +1,713 @@
+# OPUS Plan - App Clip for BusinessCard
+
+## Goal
+
+Enable recipients to scan a QR code, open an App Clip, preview a business card with photo, and save to Contacts without installing the full app.
+
+---
+
+## What OPUS Changes from CODEX
+
+| Area | CODEX Approach | OPUS Improvement |
+|------|---------------|------------------|
+| **Photo Storage** | Stores `vCardData` + separate `photoData` CKAsset | Store `vCardData` only (photo already embedded as base64) |
+| **CloudKit Database** | Suggests private, mentions public | **Public database required** for App Clip access |
+| **Open Questions** | Lists 5 unresolved questions | All resolved with sensible defaults |
+| **TTL** | "3 days vs 7 days?" | **7 days** (re-scans common, privacy still protected) |
+| **Post-Save Behavior** | "Delete immediately or retain?" | **Retain until expiry** (allows multiple recipients) |
+| **Preview Style** | "Minimal or full?" | **Minimal** (faster load, cleaner UX) |
+| **Record Consumption** | Tracks `consumedAt` | **Remove consumption tracking** (over-engineering) |
+| **Target Size** | "<15MB" | **<10MB** for noticeably faster App Clip launch |
+| **Protocols** | 3 separate protocols | **2 protocols** (combine upload/cleanup in main app) |
+| **Code Skeletons** | Placeholders | **Concrete implementations** using existing `vCardFilePayload` |
+
+---
+
+## Resolved Decisions
+
+- **Domain**: Use placeholder `cards.example.com` (update in entitlements before release)
+- **TTL**: 7 days
+- **Database**: Public (App Clips cannot access private databases)
+- **Photo Storage**: vCard only (existing `vCardFilePayload` embeds photo as base64)
+- **Post-Save**: Keep record until expired
+- **Preview**: Minimal card (photo, name, role, company)
+- **App Clip Size**: Target <10MB
+
+---
+
+## Architecture Overview
+
+```mermaid
+flowchart TB
+ subgraph MainApp [Main App]
+ ShareCardView --> AppClipShareState
+ AppClipShareState --> SharedCardCloudKitService
+ SharedCardCloudKitService --> CloudKitPublic
+ end
+
+ subgraph CloudKitPublic [CloudKit Public Database]
+ SharedCardRecord[SharedCard Record]
+ end
+
+ subgraph AppClipTarget [App Clip]
+ AppClipRootView --> AppClipCardStore
+ AppClipCardStore --> AppClipCloudKitService
+ AppClipCloudKitService --> CloudKitPublic
+ AppClipCardStore --> ContactSaveService
+ ContactSaveService --> Contacts
+ end
+
+ QRCode[QR Code] -->|scan| AppClipRootView
+ ShareCardView -->|generate| QRCode
+```
+
+---
+
+## Phase 1: CloudKit Setup (Main App)
+
+### 1.1 Enable CloudKit Capability
+
+In Xcode, ensure the iOS target has:
+- iCloud capability enabled
+- CloudKit checked
+- Container: `iCloud.com.mbrucedogs.BusinessCard`
+
+### 1.2 Create CloudKit Record Model
+
+File: `BusinessCard/Models/SharedCardRecord.swift`
+
+```swift
+import CloudKit
+
+/// Represents a shared card in CloudKit public database.
+/// NOT a SwiftData model - uses raw CKRecord for ephemeral sharing.
+struct SharedCardRecord: Sendable {
+ let recordID: CKRecord.ID
+ let vCardData: String
+ let expiresAt: Date
+ let createdAt: Date
+
+ static let recordType = "SharedCard"
+
+ enum Field: String {
+ case vCardData
+ case expiresAt
+ case createdAt
+ }
+
+ init(recordID: CKRecord.ID, vCardData: String, expiresAt: Date, createdAt: Date = .now) {
+ self.recordID = recordID
+ self.vCardData = vCardData
+ self.expiresAt = expiresAt
+ self.createdAt = createdAt
+ }
+
+ init?(record: CKRecord) {
+ guard let vCardData = record[Field.vCardData.rawValue] as? String,
+ let expiresAt = record[Field.expiresAt.rawValue] as? Date else {
+ return nil
+ }
+ self.recordID = record.recordID
+ self.vCardData = vCardData
+ self.expiresAt = expiresAt
+ self.createdAt = (record[Field.createdAt.rawValue] as? Date) ?? record.creationDate ?? .now
+ }
+
+ func toCKRecord() -> CKRecord {
+ let record = CKRecord(recordType: Self.recordType, recordID: recordID)
+ record[Field.vCardData.rawValue] = vCardData
+ record[Field.expiresAt.rawValue] = expiresAt
+ record[Field.createdAt.rawValue] = createdAt
+ return record
+ }
+}
+```
+
+### 1.3 Create Upload Result Model
+
+File: `BusinessCard/Models/SharedCardUploadResult.swift`
+
+```swift
+import Foundation
+
+struct SharedCardUploadResult: Sendable {
+ let recordName: String
+ let appClipURL: URL
+ let expiresAt: Date
+}
+```
+
+### 1.4 Create Protocol
+
+File: `BusinessCard/Protocols/SharedCardProviding.swift`
+
+```swift
+import Foundation
+
+/// Provides shared card upload and cleanup functionality.
+protocol SharedCardProviding: Sendable {
+ func uploadSharedCard(_ card: BusinessCard) async throws -> SharedCardUploadResult
+ func cleanupExpiredCards() async
+}
+```
+
+### 1.5 Create CloudKit Service
+
+File: `BusinessCard/Services/SharedCardCloudKitService.swift`
+
+```swift
+import CloudKit
+import Foundation
+
+struct SharedCardCloudKitService: SharedCardProviding {
+ private let container: CKContainer
+ private let database: CKDatabase
+ private let ttlDays: Int
+ private let appClipDomain: String
+
+ init(
+ containerID: String = "iCloud.com.mbrucedogs.BusinessCard",
+ ttlDays: Int = 7,
+ appClipDomain: String = "cards.example.com"
+ ) {
+ self.container = CKContainer(identifier: containerID)
+ self.database = container.publicCloudDatabase
+ self.ttlDays = ttlDays
+ self.appClipDomain = appClipDomain
+ }
+
+ @MainActor
+ func uploadSharedCard(_ card: BusinessCard) async throws -> SharedCardUploadResult {
+ let vCardData = card.vCardFilePayload
+ let expiresAt = Date.now.addingTimeInterval(TimeInterval(ttlDays * 24 * 60 * 60))
+ let recordID = CKRecord.ID(recordName: UUID().uuidString)
+
+ let sharedCard = SharedCardRecord(
+ recordID: recordID,
+ vCardData: vCardData,
+ expiresAt: expiresAt
+ )
+
+ let record = sharedCard.toCKRecord()
+ _ = try await database.save(record)
+
+ guard let appClipURL = URL(string: "https://\(appClipDomain)/appclip?id=\(recordID.recordName)") else {
+ throw SharedCardError.invalidURL
+ }
+
+ return SharedCardUploadResult(
+ recordName: recordID.recordName,
+ appClipURL: appClipURL,
+ expiresAt: expiresAt
+ )
+ }
+
+ func cleanupExpiredCards() async {
+ let predicate = NSPredicate(format: "expiresAt < %@", Date.now as NSDate)
+ let query = CKQuery(recordType: SharedCardRecord.recordType, predicate: predicate)
+
+ do {
+ let (results, _) = try await database.records(matching: query)
+ let recordIDs = results.compactMap { result -> CKRecord.ID? in
+ guard case .success = result.1 else { return nil }
+ return result.0
+ }
+
+ guard !recordIDs.isEmpty else { return }
+
+ let operation = CKModifyRecordsOperation(recordsToSave: nil, recordIDsToDelete: recordIDs)
+ operation.qualityOfService = .utility
+ database.add(operation)
+ } catch {
+ // Cleanup is best-effort; log but don't throw
+ }
+ }
+}
+
+enum SharedCardError: Error, LocalizedError {
+ case invalidURL
+ case uploadFailed(Error)
+ case fetchFailed(Error)
+ case recordNotFound
+ case recordExpired
+
+ var errorDescription: String? {
+ switch self {
+ case .invalidURL:
+ return String(localized: "Failed to create share URL")
+ case .uploadFailed(let error):
+ return String(localized: "Upload failed: \(error.localizedDescription)")
+ case .fetchFailed(let error):
+ return String(localized: "Could not load card: \(error.localizedDescription)")
+ case .recordNotFound:
+ return String(localized: "Card not found")
+ case .recordExpired:
+ return String(localized: "This card has expired")
+ }
+ }
+}
+```
+
+### 1.6 Add Cleanup to AppState
+
+Update `BusinessCard/State/AppState.swift` to trigger cleanup on launch:
+
+```swift
+// Add to AppState init or a dedicated startup method
+Task {
+ await SharedCardCloudKitService().cleanupExpiredCards()
+}
+```
+
+---
+
+## Phase 2: Main App UI Integration
+
+### 2.1 Create Share State
+
+File: `BusinessCard/State/AppClipShareState.swift`
+
+```swift
+import Foundation
+
+@MainActor
+@Observable
+final class AppClipShareState {
+ private let service: SharedCardProviding
+
+ var isUploading = false
+ var uploadResult: SharedCardUploadResult?
+ var errorMessage: String?
+
+ var hasAppClipURL: Bool { uploadResult != nil }
+
+ init(service: SharedCardProviding = SharedCardCloudKitService()) {
+ self.service = service
+ }
+
+ func shareViaAppClip(card: BusinessCard) async {
+ isUploading = true
+ errorMessage = nil
+ uploadResult = nil
+
+ defer { isUploading = false }
+
+ do {
+ uploadResult = try await service.uploadSharedCard(card)
+ } catch {
+ errorMessage = error.localizedDescription
+ }
+ }
+
+ func reset() {
+ uploadResult = nil
+ errorMessage = nil
+ }
+}
+```
+
+### 2.2 Update ShareCardView
+
+Add to `BusinessCard/Views/ShareCardView.swift`:
+
+- New "Share via App Clip" section
+- Upload progress indicator
+- QR code display for App Clip URL
+- Error state with retry
+
+```swift
+// Add state property
+@State private var appClipState = AppClipShareState()
+
+// Add UI section
+Section {
+ if appClipState.isUploading {
+ ProgressView()
+ .accessibilityLabel(Text("Uploading card"))
+ } else if let result = appClipState.uploadResult {
+ // Show QR code with result.appClipURL
+ QRCodeView(content: result.appClipURL.absoluteString)
+ Text("Expires in 7 days")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ } else {
+ Button("Share via App Clip") {
+ Task { await appClipState.shareViaAppClip(card: card) }
+ }
+ }
+
+ if let error = appClipState.errorMessage {
+ Text(error)
+ .foregroundStyle(.red)
+ .font(.caption)
+ }
+} header: {
+ Text("App Clip (includes photo)")
+}
+```
+
+---
+
+## Phase 3: App Clip Target
+
+### 3.1 Create App Clip Target
+
+1. File > New > Target > App Clip
+2. Name: `BusinessCardClip`
+3. Bundle ID: `com.mbrucedogs.BusinessCard.Clip`
+4. Keep size <10MB (no heavy assets)
+
+### 3.2 App Clip Folder Structure
+
+```
+BusinessCardClip/
+├── BusinessCardClipApp.swift
+├── Design/
+│ └── ClipDesignConstants.swift
+├── Models/
+│ └── SharedCardSnapshot.swift
+├── Services/
+│ ├── ClipCloudKitService.swift
+│ └── ContactSaveService.swift
+├── State/
+│ └── ClipCardStore.swift
+├── Views/
+│ ├── ClipRootView.swift
+│ └── Components/
+│ ├── ClipLoadingView.swift
+│ ├── ClipCardPreview.swift
+│ ├── ClipSuccessView.swift
+│ └── ClipErrorView.swift
+└── Resources/
+ └── Localizable.xcstrings
+```
+
+### 3.3 App Clip Models
+
+File: `BusinessCardClip/Models/SharedCardSnapshot.swift`
+
+```swift
+import Foundation
+
+struct SharedCardSnapshot: Sendable {
+ let recordName: String
+ let vCardData: String
+ let displayName: String
+ let role: String
+ let company: String
+ let photoData: Data?
+
+ init(recordName: String, vCardData: String) {
+ self.recordName = recordName
+ self.vCardData = vCardData
+
+ // Parse display fields from vCard
+ let lines = vCardData.components(separatedBy: .newlines)
+ self.displayName = Self.parseField("FN:", from: lines) ?? "Contact"
+ self.role = Self.parseField("TITLE:", from: lines) ?? ""
+ self.company = Self.parseField("ORG:", from: lines)?.components(separatedBy: ";").first ?? ""
+ self.photoData = Self.parsePhoto(from: lines)
+ }
+
+ private static func parseField(_ prefix: String, from lines: [String]) -> String? {
+ lines.first { $0.hasPrefix(prefix) }?.dropFirst(prefix.count).trimmingCharacters(in: .whitespaces)
+ }
+
+ private static func parsePhoto(from lines: [String]) -> Data? {
+ guard let photoLine = lines.first(where: { $0.hasPrefix("PHOTO;") }),
+ let base64Start = photoLine.range(of: ":")?.upperBound else {
+ return nil
+ }
+ let base64String = String(photoLine[base64Start...])
+ return Data(base64Encoded: base64String)
+ }
+}
+```
+
+### 3.4 App Clip CloudKit Service
+
+File: `BusinessCardClip/Services/ClipCloudKitService.swift`
+
+```swift
+import CloudKit
+
+struct ClipCloudKitService: Sendable {
+ private let database: CKDatabase
+
+ init(containerID: String = "iCloud.com.mbrucedogs.BusinessCard") {
+ self.database = CKContainer(identifier: containerID).publicCloudDatabase
+ }
+
+ func fetchSharedCard(recordName: String) async throws -> SharedCardSnapshot {
+ let recordID = CKRecord.ID(recordName: recordName)
+
+ let record: CKRecord
+ do {
+ record = try await database.record(for: recordID)
+ } catch {
+ throw ClipError.fetchFailed
+ }
+
+ guard let vCardData = record["vCardData"] as? String else {
+ throw ClipError.invalidRecord
+ }
+
+ if let expiresAt = record["expiresAt"] as? Date, expiresAt < .now {
+ throw ClipError.expired
+ }
+
+ return SharedCardSnapshot(recordName: recordName, vCardData: vCardData)
+ }
+}
+
+enum ClipError: Error, LocalizedError {
+ case fetchFailed
+ case invalidRecord
+ case expired
+ case contactSaveFailed
+ case contactsAccessDenied
+
+ var errorDescription: String? {
+ switch self {
+ case .fetchFailed: return String(localized: "Could not load card")
+ case .invalidRecord: return String(localized: "Invalid card data")
+ case .expired: return String(localized: "This card has expired")
+ case .contactSaveFailed: return String(localized: "Failed to save contact")
+ case .contactsAccessDenied: return String(localized: "Contacts access required")
+ }
+ }
+}
+```
+
+### 3.5 Contact Save Service
+
+File: `BusinessCardClip/Services/ContactSaveService.swift`
+
+```swift
+import Contacts
+
+struct ContactSaveService: Sendable {
+ func saveContact(vCardData: String) async throws {
+ let store = CNContactStore()
+
+ let authorized = try await store.requestAccess(for: .contacts)
+ guard authorized else {
+ throw ClipError.contactsAccessDenied
+ }
+
+ guard let data = vCardData.data(using: .utf8),
+ let contact = try CNContactVCardSerialization.contacts(with: data).first else {
+ throw ClipError.invalidRecord
+ }
+
+ let mutableContact = contact.mutableCopy() as? CNMutableContact ?? CNMutableContact()
+ let saveRequest = CNSaveRequest()
+ saveRequest.add(mutableContact, toContainerWithIdentifier: nil)
+
+ do {
+ try store.execute(saveRequest)
+ } catch {
+ throw ClipError.contactSaveFailed
+ }
+ }
+}
+```
+
+### 3.6 App Clip State
+
+File: `BusinessCardClip/State/ClipCardStore.swift`
+
+```swift
+import Foundation
+
+@MainActor
+@Observable
+final class ClipCardStore {
+ enum State {
+ case loading
+ case loaded(SharedCardSnapshot)
+ case saved
+ case error(String)
+ }
+
+ private let cloudKit: ClipCloudKitService
+ private let contactSave: ContactSaveService
+
+ var state: State = .loading
+
+ var snapshot: SharedCardSnapshot? {
+ if case .loaded(let snap) = state { return snap }
+ return nil
+ }
+
+ init(cloudKit: ClipCloudKitService = ClipCloudKitService(),
+ contactSave: ContactSaveService = ContactSaveService()) {
+ self.cloudKit = cloudKit
+ self.contactSave = contactSave
+ }
+
+ func load(recordName: String) async {
+ state = .loading
+ do {
+ let snapshot = try await cloudKit.fetchSharedCard(recordName: recordName)
+ state = .loaded(snapshot)
+ } catch {
+ state = .error(error.localizedDescription)
+ }
+ }
+
+ func saveToContacts() async {
+ guard let snapshot else { return }
+ do {
+ try await contactSave.saveContact(vCardData: snapshot.vCardData)
+ state = .saved
+ } catch {
+ state = .error(error.localizedDescription)
+ }
+ }
+}
+```
+
+### 3.7 App Clip Root View
+
+File: `BusinessCardClip/Views/ClipRootView.swift`
+
+```swift
+import SwiftUI
+
+struct ClipRootView: View {
+ @State private var store = ClipCardStore()
+ let recordName: String
+
+ var body: some View {
+ Group {
+ switch store.state {
+ case .loading:
+ ClipLoadingView()
+ case .loaded(let snapshot):
+ ClipCardPreview(snapshot: snapshot) {
+ Task { await store.saveToContacts() }
+ }
+ case .saved:
+ ClipSuccessView()
+ case .error(let message):
+ ClipErrorView(message: message) {
+ Task { await store.load(recordName: recordName) }
+ }
+ }
+ }
+ .task {
+ await store.load(recordName: recordName)
+ }
+ }
+}
+```
+
+### 3.8 App Clip Entry Point
+
+File: `BusinessCardClip/BusinessCardClipApp.swift`
+
+```swift
+import SwiftUI
+
+@main
+struct BusinessCardClipApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
+ handleUserActivity(activity)
+ }
+ }
+ }
+
+ private func handleUserActivity(_ activity: NSUserActivity) {
+ guard let url = activity.webpageURL,
+ let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
+ let recordName = components.queryItems?.first(where: { $0.name == "id" })?.value else {
+ return
+ }
+ // Pass recordName to root view via environment or state
+ }
+}
+```
+
+---
+
+## Phase 4: Configuration
+
+### 4.1 Associated Domains
+
+Add to both iOS app and App Clip entitlements:
+
+```
+appclips:cards.example.com
+```
+
+### 4.2 Server Configuration
+
+Create `.well-known/apple-app-site-association` on `cards.example.com`:
+
+```json
+{
+ "appclips": {
+ "apps": ["TEAMID.com.mbrucedogs.BusinessCard.Clip"]
+ }
+}
+```
+
+### 4.3 App Store Connect
+
+1. Register App Clip in App Store Connect
+2. Configure App Clip Experience:
+ - Title: "BusinessCard"
+ - Subtitle: "View and save contact"
+ - Action: "Open"
+ - Invocation URL: `https://cards.example.com/appclip`
+
+---
+
+## Files to Create
+
+| File | Location |
+|------|----------|
+| `SharedCardRecord.swift` | `BusinessCard/Models/` |
+| `SharedCardUploadResult.swift` | `BusinessCard/Models/` |
+| `SharedCardProviding.swift` | `BusinessCard/Protocols/` |
+| `SharedCardCloudKitService.swift` | `BusinessCard/Services/` |
+| `AppClipShareState.swift` | `BusinessCard/State/` |
+| `SharedCardSnapshot.swift` | `BusinessCardClip/Models/` |
+| `ClipCloudKitService.swift` | `BusinessCardClip/Services/` |
+| `ContactSaveService.swift` | `BusinessCardClip/Services/` |
+| `ClipCardStore.swift` | `BusinessCardClip/State/` |
+| `ClipRootView.swift` | `BusinessCardClip/Views/` |
+| Component views | `BusinessCardClip/Views/Components/` |
+
+## Files to Update
+
+| File | Changes |
+|------|---------|
+| `ShareCardView.swift` | Add App Clip share section |
+| `AppState.swift` | Add cleanup call on init |
+| `BusinessCard.entitlements` | Add Associated Domains |
+
+---
+
+## Testing Strategy
+
+1. **Unit Tests**: CloudKit upload/fetch logic, vCard parsing, error handling
+2. **Integration Tests**: Full upload/fetch cycle (requires CloudKit)
+3. **Device Tests**: App Clip invocation via QR scan, contact save
+
+---
+
+## Documentation Updates
+
+When implementation begins, update:
+- `README.md` - Add App Clip feature description
+- `ai_implementation.md` - Add CloudKit and App Clip architecture
+- `ROADMAP.md` - Mark App Clip phases as completed
+
+---
+
+*Last updated: January 10, 2026*