Troubleshooting¶
This guide covers the most common issues encountered during development, deployment, and production operation of Nutri-E.
Table of Contents¶
- iOS Build Errors
- Xcode Cloud Failures
- Cloudflare Worker Deployment
- Worker Runtime Errors
- Subscription / RevenueCat
- HealthKit Permissions
- Device Auth / Token Issues
- Escalation
iOS Build Errors¶
Missing Info.plist values at build time¶
Symptom: Build fails with Info.plist file is missing or key not found errors.
Cause: Info.plist is generated from Info.plist.template and requires environment variables injected by CI. Running a local build without those vars causes missing keys.
Fix:
# Copy and fill in the template
cp ios/Nutri-E/Info.plist.template ios/Nutri-E/Nutri-E/Info.plist
# Fill OPENAI_WORKER_URL, DSLD_WORKER_URL, etc. with sandbox values
Info.plist — it contains environment-specific URLs.
GoogleService-Info.plist not found¶
Symptom: Build error: Could not find the file 'GoogleService-Info.plist'.
Cause: Firebase config file is .gitignored. Required for any build that uses Firebase (Crashlytics, Analytics).
Fix:
1. Download from Firebase Console → Nutri-E project → iOS app
2. Place at ios/Nutri-E/Nutri-E/GoogleService-Info.plist
HealthKit entitlement build error¶
Symptom: Entitlements file do not match or HealthKit capability missing in provisioning profile.
Cause: The entitlements file (Nutri-E.entitlements) declares com.apple.developer.healthkit = true. If the provisioning profile doesn't include HealthKit, signing fails.
Fix:
- Ensure the provisioning profile (App Store Connect → Certificates, Identifiers & Profiles) has HealthKit enabled for no.invotek.NutriE
- Regenerate profile if needed, then download and update in Xcode
Package.resolved conflicts / SPM cache issues¶
Symptom: error: package manifest loading failed, duplicate package errors, or resolution timeout.
Fix:
# Reset SPM cache
cd ios/Nutri-E
rm -rf ~/Library/Caches/org.swift.swiftpm
rm -rf .build
# Reopen Xcode and let it resolve packages
413 Request Too Large when reading images¶
Symptom: Claude Code crashes mid-task with API error when reading PNG files.
Cause: Read tool has a ~1-2 MB limit. Marketing screenshots and HEIC exports often exceed this.
Fix:
# Resize before reading
convert large-image.png -resize 800x800\> -quality 85 large-image.png
# Or check size first
ls -lh image.png
sips -g pixelWidth -g pixelHeight image.png
Xcode Cloud Failures¶
Build triggers but doesn't reach TestFlight¶
Symptom: Xcode Cloud shows green but no build appears in TestFlight.
Common causes and fixes:
- Missing export options — Check
ios/Nutri-E/ExportOptions.plistexists and has correct provisioning profile names - Build number not incrementing — Xcode Cloud manages its own build counter (separate from local project version). Check Xcode Cloud → Settings → Build Number for the current counter
- Missing
ci_pre_xcodebuild.sh— The script atios/ci_scripts/ci_pre_xcodebuild.shinstalls dependencies and injects secrets. Ensure it's executable:chmod +x ios/ci_scripts/ci_pre_xcodebuild.sh
Xcode Cloud can't find GoogleService-Info.plist¶
Symptom: Cloud build fails with Firebase config missing.
Fix: The ci_pre_xcodebuild.sh script must write this file from a CI environment variable (GOOGLE_SERVICE_INFO_PLIST_BASE64). Set this secret in Xcode Cloud → Workflow → Environment Variables as a base64-encoded version of the file:
ARC runner (arc-linux-nutri-e) not picking up jobs¶
Symptom: GitHub Actions workflow shows "Waiting for a runner" indefinitely.
Fix:
1. Check ARC runner health in tablez-gitops cluster: kubectl get pods -n arc-runners
2. If pod is CrashLoopBackOff, restart: kubectl rollout restart deployment arc-linux-nutri-e -n arc-runners
3. Verify github-pat secret in arc-runners namespace hasn't expired
deploy-workers.yml deploys sandbox but skips production¶
Symptom: Sandbox deploys succeed; production stage shows as skipped.
Cause: Production deploy requires sandbox health check to pass first (sequential stages in the workflow). A failing sandbox health check blocks production.
Fix: Check the sandbox health check step output in the failed run. Common issue: worker deployed but KV namespace IDs in wrangler.toml don't match the Cloudflare account's actual namespace IDs.
Cloudflare Worker Deployment¶
wrangler deploy fails with authentication error¶
Symptom: ✘ [ERROR] Authentication error.
Fix:
wrangler login # opens browser OAuth flow
# Or use API token:
export CLOUDFLARE_API_TOKEN=<your-token>
wrangler deploy
KV namespace ID mismatch¶
Symptom: Worker deploys but returns KV namespace not found at runtime, or wrangler deploy fails with namespace binding error.
Cause: wrangler.toml contains hardcoded namespace IDs that belong to a specific Cloudflare account. IDs differ between accounts and environments.
Fix:
# List your actual namespace IDs
wrangler kv namespace list
# Update wrangler.toml with your IDs:
# [[kv_namespaces]]
# binding = "SUBSCRIPTIONS"
# id = "<your-actual-id>"
# preview_id = "<your-sandbox-id>"
Worker secret not set after deploy¶
Symptom: Worker returns 401 Unauthorized or DEVICE_SALT is undefined.
Cause: Secrets must be set separately from wrangler deploy. They aren't in wrangler.toml.
Fix:
cd cloudflare-worker-openai
wrangler secret put OPENAI_API_KEY # prompts for value
wrangler secret put DEVICE_SALT
wrangler secret put REVENUECAT_WEBHOOK_SECRET # apple worker only
# Verify secrets are set
wrangler secret list
Deploying to wrong environment (sandbox vs production)¶
Symptom: TestFlight app hits production quota limits, or App Store users get sandbox rates.
Fix: Always specify the environment explicitly:
Check which URL the iOS app is using: sandbox builds use nutrie-*-sandbox-v3.invotekas.workers.dev, production uses nutrie-*-v3.invotekas.workers.dev.
Worker Runtime Errors¶
401 — Invalid device signature¶
Symptom: API calls from app return {"error": "Invalid device signature"}.
Cause: Device auth uses HMAC-SHA256(deviceId + timestamp + DEVICE_SALT). Signature mismatch means either wrong salt, clock skew >60s, or device ID format mismatch.
Debugging steps:
1. Check that DEVICE_SALT secret matches between sandbox and production workers
2. Verify device clock isn't skewed — the worker rejects requests where |now - timestamp| > 60s
3. In the app: Settings → Developer Tools → Device Info → copy Device ID and compare with what the worker receives in logs (wrangler tail)
429 — Rate limit exceeded¶
Symptom: App returns rate limit error mid-session.
Cause: KV-based daily quota (OpenAI: 10–500 req/day; DSLD: 100–unlimited, depending on subscription tier). Quota resets at midnight UTC.
Debugging:
# Check quota for a device
wrangler kv key get --namespace-id=<RATE_LIMIT_ID> "rate_limit_openai_<deviceId>"
wrangler kv key get --namespace-id=<RATE_LIMIT_ID> "rate_limit_dsld_<deviceId>"
Reset for testing:
OpenAI worker returns 503 — Vision AI service temporarily unavailable¶
Symptom: Label scanning or food analysis fails with service error.
Cause: OpenAI API error (quota, outage, or invalid key).
Debugging:
# Tail worker logs
wrangler tail nutrie-openai-worker-v3
# Check OpenAI status
open https://status.openai.com
# Verify API key is valid
curl https://api.openai.com/v1/models \
-H "Authorization: Bearer $OPENAI_API_KEY" | jq '.data | length'
If key is valid but quota exceeded, check OpenAI billing.
Apple webhook worker returns JWT verification failed¶
Symptom: Subscription purchases aren't being recorded. Worker logs show JWT signature verification FAILED.
Cause: The Apple webhook worker verifies JWT signatures against Apple's root CA. Failures happen when:
1. The payload format changed (Apple updated their JWT structure)
2. REVENUECAT_WEBHOOK_SECRET doesn't match what's configured in App Store Connect / RevenueCat
3. Clock skew on the worker (rare, Cloudflare handles NTP)
Fix:
1. Confirm RevenueCat is set to send webhooks to the correct worker URL (sandbox vs production)
2. Re-check REVENUECAT_WEBHOOK_SECRET matches between RevenueCat dashboard and wrangler secret
3. Test with the included script: bash cloudflare-worker-apple/test-apple-webhook.sh
Subscription / RevenueCat¶
User purchased but app still shows free tier¶
Cause: The subscription flow is asynchronous. Apple sends the webhook to Cloudflare ~1 minute after purchase. Until then, the SUBSCRIPTIONS KV has no entry for the device.
Expected behaviour: The StoreKitManager updates local AIUsageTracker immediately for UI feedback, but the worker enforces quotas from KV. Wait 1–2 minutes and retry.
If still not resolved after 5 minutes:
1. Check worker logs for the device's purchase event: wrangler tail nutrie-apple-webhook-worker-v3
2. Verify the device ID was passed correctly as appAccountToken during purchase
3. Check SUBSCRIPTIONS KV directly:
Subscription shows active in RevenueCat but not in app¶
Cause: RevenueCat and the Cloudflare KV are independent systems. RevenueCat is the source of truth for billing; SUBSCRIPTIONS KV is the source of truth for the API workers.
Fix: Manually inspect and if needed, manually set the KV entry:
# Check what's in KV
wrangler kv key get --namespace-id=<SUBSCRIPTIONS_ID> "<deviceId>"
# Manually set subscription (temporary fix while investigating root cause)
wrangler kv key put --namespace-id=<SUBSCRIPTIONS_ID> "<deviceId>" \
'{"plan":"pro","expiresAt":"2026-12-31T00:00:00Z","updatedAt":"2026-03-15T00:00:00Z"}'
Cancellation not being processed¶
Cause: Cancellation events come through the Apple webhook. The worker moves the record to SUBSCRIPTIONS_DELETED KV and removes from SUBSCRIPTIONS.
Debugging:
1. Check SUBSCRIPTIONS_DELETED KV for the device record
2. If missing, the webhook may not have arrived — check App Store Connect → Subscriptions → server notifications log
3. Ensure the webhook URL in App Store Connect points to the correct worker environment (sandbox URL for sandbox, production URL for production)
Product IDs not loading in SubscriptionView¶
Symptom: Subscription page is blank or shows loading spinner indefinitely.
Cause: StoreKit 2 can't find the products. Common in simulator or when products are in Ready to Submit state (not yet approved).
Fix:
1. Test on a real device with a Sandbox Apple ID
2. Confirm product IDs in App Store Connect match exactly: no.invotek.nutrie.basic_monthly, pro_monthly, premium_monthly, ultimate_monthly
3. Products must be in Approved or Ready for Sale state (not Missing Metadata)
4. In simulator, use StoreKit configuration file for local testing
HealthKit Permissions¶
HealthKit data not saving after user grants permission¶
Symptom: User taps Allow in HealthKit permission sheet, but no data appears in Health app.
Cause: HealthKit permission is granted per-type. The entitlements file has com.apple.developer.healthkit.access as an empty array (no pre-declared types), meaning all types require runtime authorisation.
Fix (in code):
Confirm HealthKitService.requestAuthorization() is called before any write attempt and that the result is checked:
HealthKit permission prompt not appearing¶
Symptom: App never shows the HealthKit permission sheet.
Causes: 1. HealthKit not available on the device (iPad without health data, simulator) 2. Permission was previously denied — iOS doesn't re-prompt; user must go to Settings
Fix:
1. Check HKHealthStore.isHealthDataAvailable() before calling requestAuthorization()
2. If previously denied: Settings → Privacy & Security → Health → Nutri-E → re-enable
3. On simulator: HealthKit is not available — test on a real device
aps-environment entitlement mismatch¶
Symptom: Push notifications work on TestFlight but not in development builds, or vice versa.
Cause: Nutri-E.entitlements has aps-environment = development. Production (App Store / TestFlight) builds must use production.
Fix: Xcode automatically uses the correct environment when signing with a distribution provisioning profile. For local dev builds, development is correct. If push isn't working in a distribution build, verify the provisioning profile is a distribution (not development) profile.
Device Auth / Token Issues¶
All API calls return 401 after app update¶
Symptom: After updating the app, every request returns 401.
Cause: DEVICE_SALT was rotated on the worker after the previous release. Old app version uses the old signature scheme.
Fix:
1. Check that DEVICE_SALT matches between the version the app was compiled against and the deployed worker
2. If salt was intentionally rotated: old app versions will need to update. Post a forced-update notice via RevenueCat or App Store phased release
Device ID changes between sessions¶
Symptom: User loses their quota reset or subscription state after reinstalling.
Cause: Device ID is stored in UserDefaults (not Keychain). Reinstall wipes UserDefaults.
Known limitation: This is by design for the current implementation. A returning user after reinstall gets a fresh device ID and starts at free tier until the next Apple webhook fires and links by appAccountToken.
X-Device-Token vs X-Device-ID header confusion¶
Symptom: Worker logs show auth failures even with correct credentials.
Cause: Workers accept both X-Device-Token (preferred, rotation-friendly) and X-App-ID + X-Device-ID (legacy). The iOS app must use one scheme consistently — mixing headers causes auth failure.
Fix: Check which header scheme the current iOS build sends (search codebase for X-Device-Token). Ensure the worker's expected scheme matches.
Escalation¶
If you can't resolve an issue after following the steps above:
| Issue type | Contact / Resource |
|---|---|
| Cloudflare Worker / KV / D1 | Cloudflare status · Cloudflare Discord |
| OpenAI API outage | OpenAI status · OpenAI platform dashboard |
| Apple webhook / App Store Connect | Apple developer forums · App Store Connect → Agreements, Tax, Banking |
| RevenueCat subscription data | RevenueCat dashboard → customer lookup by device ID |
| HealthKit / StoreKit 2 bugs | Apple TSI (Technical Support Incident) via developer account |
| Xcode Cloud build issues | Xcode Cloud dashboard → build logs · Apple Developer Forums |
| CI runner down (ARC) | Check tablez-gitops cluster: kubectl get pods -n arc-runners · contact Stig-Johnny |
| Security issue (key leak, exploit) | Stig-Johnny directly — do not file a public GitHub issue |