# App Clip Deployment Guide This guide walks you through setting up the domain, Cloudflare Worker, and App Store Connect configuration needed for the BusinessCard App Clip to work. **Total Cost: ~$10-15/year** (domain only - everything else is free) **Time: ~1-2 hours** --- ## Overview ``` User scans QR code ↓ https://yourdomain.com/card/abc123 ↓ Cloudflare Worker receives request ↓ ┌───────────────────────────────────────┐ │ Serves AASA file to Apple │ │ (verifies App Clip ownership) │ └───────────────────────────────────────┘ ↓ ┌───────────┴───────────┐ ↓ ↓ iOS Android/Browser ↓ ↓ App Clip launches Worker fetches vCard (native experience) from CloudKit, returns .vcf file download ``` --- ## Phase 1: Register a Domain (~10 minutes) ### Step 1.1: Choose a Domain Name Pick something short and memorable. Examples: - `cards.yourlastname.com` (if you own a personal domain) - `bizcards.io` - `cardshare.app` - `vcards.link` ### Step 1.2: Register the Domain **Recommended Registrars** (cheapest, no upselling): | Registrar | .com Price | .io Price | .app Price | |-----------|------------|-----------|------------| | [Cloudflare Registrar](https://www.cloudflare.com/products/registrar/) | ~$10/yr | ~$35/yr | ~$15/yr | | [Porkbun](https://porkbun.com) | ~$10/yr | ~$33/yr | ~$15/yr | | [Namecheap](https://namecheap.com) | ~$11/yr | ~$35/yr | ~$16/yr | **Recommendation**: Register directly with Cloudflare - simplest setup since you'll use Cloudflare Workers anyway. ### Step 1.3: If Not Using Cloudflare Registrar If you registered elsewhere, add the domain to Cloudflare: 1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com) 2. Click "Add a Site" 3. Enter your domain 4. Select the **Free** plan 5. Cloudflare will give you 2 nameservers (e.g., `ada.ns.cloudflare.com`) 6. Go to your registrar and update nameservers to Cloudflare's 7. Wait for DNS propagation (5 min - 24 hours, usually fast) --- ## Phase 2: Create CloudKit Public Database Schema (~15 minutes) The App Clip needs to read shared cards from CloudKit's public database. ### Step 2.1: Open CloudKit Console 1. Go to [CloudKit Console](https://icloud.developer.apple.com/dashboard) 2. Sign in with your Apple Developer account 3. Select your container: `iCloud.com.mbrucedogs.BusinessCard` ### Step 2.2: Create the Record Type 1. Click **Schema** in the sidebar 2. Click **Record Types** 3. Click **+** to create new record type 4. Name: `SharedCard` 5. Add these fields: | Field Name | Type | |------------|------| | `vCardData` | String | | `expiresAt` | Date/Time | | `createdAt` | Date/Time | 6. Click **Save** ### Step 2.3: Set Security Roles (IMPORTANT) 1. Click **Security Roles** under your record type 2. For the **Public** database: - **Authenticated Users**: Read, Write (for uploading from main app) - **World**: Read (for App Clip to fetch without sign-in) 3. Click **Save** ### Step 2.4: Deploy to Production 1. Click **Deploy Schema to Production...** 2. Confirm the deployment --- ## Phase 3: Set Up Cloudflare Worker (~30 minutes) ### Step 3.1: Create the Worker 1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com) 2. Click **Workers & Pages** in the sidebar 3. Click **Create Application** 4. Click **Create Worker** 5. Name it: `businesscard-share` (or similar) 6. Click **Deploy** (creates with placeholder code) ### Step 3.2: Configure Worker Code 1. Click **Edit Code** (or "Quick Edit") 2. Replace all code with: ```javascript // BusinessCard App Clip & vCard Worker // Handles iOS App Clip verification and Android vCard downloads // ============================================================================ // CONFIGURATION - Update these values // ============================================================================ const CONFIG = { // Your Apple Developer Team ID (find in Apple Developer portal) teamId: '6R7KLBPBLZ', // Your App Clip bundle identifier appClipBundleId: 'com.mbrucedogs.BusinessCard.Clip', // Your CloudKit container identifier cloudKitContainer: 'iCloud.com.mbrucedogs.BusinessCard', // CloudKit environment ('development' or 'production') cloudKitEnvironment: 'production', }; // ============================================================================ // MAIN HANDLER // ============================================================================ export default { async fetch(request, env, ctx) { const url = new URL(request.url); // Handle AASA file for Apple App Clip verification if (url.pathname === '/.well-known/apple-app-site-association') { return handleAASA(); } // Handle card requests (both App Clip and vCard download) if (url.pathname === '/appclip' || url.pathname.startsWith('/card/')) { return handleCardRequest(request, url); } // Handle root - show a simple landing page if (url.pathname === '/') { return handleLandingPage(); } return new Response('Not Found', { status: 404 }); } }; // ============================================================================ // AASA HANDLER - Apple App Clip Verification // ============================================================================ function handleAASA() { const aasa = { appclips: { apps: [`${CONFIG.teamId}.${CONFIG.appClipBundleId}`] } }; return new Response(JSON.stringify(aasa), { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=86400', // Cache for 24 hours } }); } // ============================================================================ // CARD REQUEST HANDLER // ============================================================================ async function handleCardRequest(request, url) { // Extract record ID from URL // Supports: /appclip?id=xxx or /card/xxx let recordId = url.searchParams.get('id'); if (!recordId && url.pathname.startsWith('/card/')) { recordId = url.pathname.split('/')[2]; } if (!recordId) { return new Response('Missing card ID', { status: 400 }); } // Check if this is an iOS device (App Clip will handle it natively) const userAgent = request.headers.get('User-Agent') || ''; const isIOS = /iPhone|iPad|iPod/.test(userAgent); // For iOS, we could return a page that triggers App Clip, // but iOS typically intercepts the URL before reaching here. // This fallback is mainly for Android/desktop browsers. if (isIOS) { // Return a simple page that might help if App Clip doesn't auto-launch return handleIOSFallback(recordId); } // For non-iOS devices, fetch the vCard from CloudKit and serve it return handleVCardDownload(recordId); } // ============================================================================ // iOS FALLBACK - In case App Clip doesn't auto-launch // ============================================================================ function handleIOSFallback(recordId) { const html = ` Opening BusinessCard...

Opening BusinessCard

If the app doesn't open automatically, make sure you have iOS 14 or later.

`; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } // ============================================================================ // VCARD DOWNLOAD - For Android/Desktop browsers // ============================================================================ async function handleVCardDownload(recordId) { try { // Fetch the record from CloudKit public database const vCardData = await fetchFromCloudKit(recordId); if (!vCardData) { return handleNotFound(); } // Extract name from vCard for filename const nameMatch = vCardData.match(/FN:(.+)/); const name = nameMatch ? nameMatch[1].trim().replace(/[^a-zA-Z0-9]/g, '_') : 'contact'; // Return as downloadable vCard file return new Response(vCardData, { headers: { 'Content-Type': 'text/vcard; charset=utf-8', 'Content-Disposition': `attachment; filename="${name}.vcf"`, 'Cache-Control': 'private, max-age=300', // Cache for 5 minutes } }); } catch (error) { console.error('Error fetching vCard:', error); return handleError(); } } // ============================================================================ // CLOUDKIT FETCH // ============================================================================ async function fetchFromCloudKit(recordName) { // CloudKit Web Services public database query // Note: For public database reads, no authentication is needed const ckURL = `https://api.apple-cloudkit.com/database/1/${CONFIG.cloudKitContainer}/${CONFIG.cloudKitEnvironment}/public/records/lookup`; const response = await fetch(ckURL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ records: [{ recordName }] }) }); if (!response.ok) { console.error('CloudKit error:', await response.text()); return null; } const data = await response.json(); if (!data.records || data.records.length === 0) { return null; } const record = data.records[0]; // Check if expired if (record.fields?.expiresAt?.value) { const expiresAt = new Date(record.fields.expiresAt.value); if (expiresAt < new Date()) { return null; // Expired } } return record.fields?.vCardData?.value || null; } // ============================================================================ // ERROR PAGES // ============================================================================ function handleNotFound() { const html = ` Card Not Found

😕

Card Not Found

This business card may have expired or been removed. Shared cards are available for 7 days.

`; return new Response(html, { status: 404, headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } function handleError() { const html = ` Error

⚠️

Something Went Wrong

We couldn't load this business card. Please try again or ask the sender for a new link.

`; return new Response(html, { status: 500, headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } // ============================================================================ // LANDING PAGE // ============================================================================ function handleLandingPage() { const html = ` BusinessCard

📇 BusinessCard

Share your digital business card with anyone, even if they don't have the app.

`; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } ``` 3. Click **Save and Deploy** ### Step 3.3: Update Configuration Values In the worker code, update these values at the top: ```javascript const CONFIG = { teamId: '6R7KLBPBLZ', // Your Team ID from Apple Developer Portal appClipBundleId: 'com.mbrucedogs.BusinessCard.Clip', // From Base.xcconfig cloudKitContainer: 'iCloud.com.mbrucedogs.BusinessCard', // From Base.xcconfig cloudKitEnvironment: 'production', // or 'development' for testing }; ``` ### Step 3.4: Add Custom Domain to Worker 1. In the Worker settings, click **Triggers** tab 2. Click **Add Custom Domain** 3. Enter your domain (e.g., `cards.yourdomain.com` or just `yourdomain.com`) 4. Cloudflare will automatically configure DNS ### Step 3.5: Verify AASA File Open in browser: `https://yourdomain.com/.well-known/apple-app-site-association` You should see: ```json {"appclips":{"apps":["6R7KLBPBLZ.com.mbrucedogs.BusinessCard.Clip"]}} ``` --- ## Phase 4: Configure App Store Connect (~20 minutes) ### Step 4.1: Register App Clip Bundle ID 1. Go to [Apple Developer Portal](https://developer.apple.com/account) 2. Click **Certificates, Identifiers & Profiles** 3. Click **Identifiers** → **+** 4. Select **App IDs** → **App Clip** 5. Enter: - Description: `BusinessCard App Clip` - Bundle ID: `com.mbrucedogs.BusinessCard.Clip` 6. Enable capabilities: - **Associated Domains** ✅ - **iCloud** (CloudKit) ✅ 7. Click **Continue** → **Register** ### Step 4.2: Create App Store Connect Record 1. Go to [App Store Connect](https://appstoreconnect.apple.com) 2. Select your BusinessCard app 3. Under **App Clip** section (or create if first time) 4. Configure the App Clip Experience: - **Title**: BusinessCard - **Subtitle**: View & save contact - **Action**: Open ### Step 4.3: Add Invocation URLs 1. In App Clip settings, click **Advanced App Clip Experiences** 2. Add your URL pattern: - URL: `https://yourdomain.com/appclip` - Action: Open --- ## Phase 5: Update Xcode Project (~10 minutes) ### Step 5.1: Update Base.xcconfig Open `BusinessCard/Configuration/Base.xcconfig` and update: ``` APPCLIP_DOMAIN = yourdomain.com ``` Replace `yourdomain.com` with your actual domain. ### Step 5.2: Build and Upload to TestFlight 1. Select **BusinessCard** scheme (main app - it includes the App Clip) 2. Product → Archive 3. Distribute to App Store Connect 4. Upload to TestFlight ### Step 5.3: Test with TestFlight 1. Install the app via TestFlight on a real device 2. Open the app → Share tab 3. Generate an App Clip link for a card 4. Scan the QR code with another iOS device's camera 5. App Clip should launch and show the card preview --- ## Phase 6: Testing Checklist ### Local Testing (Simulator) - [ ] Main app builds successfully - [ ] App Clip target builds successfully - [ ] Can generate App Clip URL in Share view - [ ] CloudKit record is created ### TestFlight Testing - [ ] App + App Clip installed via TestFlight - [ ] QR code scans and App Clip launches - [ ] Card preview shows correctly with photo - [ ] "Save to Contacts" works - [ ] Android device downloads .vcf file from same URL ### Production Readiness - [ ] AASA file accessible at domain - [ ] CloudKit production schema deployed - [ ] App Clip Experience configured in App Store Connect --- ## Troubleshooting ### App Clip Doesn't Launch When Scanning QR 1. **Check AASA file**: Visit `https://yourdomain.com/.well-known/apple-app-site-association` 2. **Validate with Apple**: Use [AASA Validator](https://branch.io/resources/aasa-validator/) 3. **Check entitlements**: Ensure `appclips:yourdomain.com` is in both app and clip entitlements 4. **Wait for cache**: Apple caches AASA files; may take up to 24 hours ### CloudKit Fetch Fails 1. **Check environment**: Using `development` vs `production`? 2. **Check permissions**: Public database needs "World: Read" permission 3. **Check record exists**: Verify in CloudKit Console ### Android vCard Download Fails 1. **Check Cloudflare Worker logs**: Workers → your-worker → Logs 2. **Test CloudKit endpoint**: Try the fetch manually in worker 3. **Check CORS**: Shouldn't be needed for downloads but verify --- ## Summary | Step | What | Time | |------|------|------| | 1 | Register domain | 10 min | | 2 | CloudKit schema | 15 min | | 3 | Cloudflare Worker | 30 min | | 4 | App Store Connect | 20 min | | 5 | Xcode + TestFlight | 10 min | | 6 | Testing | 15 min | | **Total** | | **~2 hours** | **Ongoing Cost**: ~$10-15/year (domain only) --- *Last updated: January 10, 2026*