Going Live
Troubleshooting & FAQ
Most "it won't build" or "the AI just errors out" moments with ShipThatApp trace back to one of a handful of setup steps — a missing key in Config.xcconfig, a backend secret that doesn't match, or an Apple-side capability that isn't wired up yet. This page is the fast lane: each entry is symptom → cause → fix, drawn from how the kit actually behaves so you can match the error you're seeing and move on.
If you haven't done first-run setup yet, start with Installation & Setup and Configurations — almost everything below assumes a populated Config.xcconfig.
The 30-second checklist
Nine out of ten setup failures are one of these:
ShipThatApp/Support/Config.xcconfigdoesn't exist yet (you only haveConfig.Example.xcconfig).- A value in it is still a placeholder (
YOUR_…). API_AUTH_KEYin the app doesn't matchAUTH_SECRET_KEYon the backend.- The backend proxy isn't deployed/reachable.
Confirm those four before debugging anything deeper.
Build & toolchain
"Unsupported SDK" or SwiftUI APIs won't compile
Symptom. The project won't build on a clean clone — errors about an unsupported SDK, unavailable SwiftUI symbols, or LocalLLMService / Foundation Models APIs not existing.
Cause. The kit targets the iOS 26 SDK (IPHONEOS_DEPLOYMENT_TARGET = 26.0) and uses current-generation SwiftUI and Apple Foundation Models. An older Xcode simply doesn't ship that SDK.
Fix. Install the latest Xcode (the one bundling the iOS 26 SDK) and select it:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
xcodebuild -version
Then build for an iOS 26 simulator or device. On-device AI (LocalLLMService) needs a device/simulator running iOS 26 with Apple Intelligence available; the cloud AI features work regardless.
"Config.xcconfig not found" / signing or scheme noise
Symptom. Build fails referencing a missing Config.xcconfig, or you get signing errors before the app even runs.
Cause. Config.xcconfig is gitignored on purpose so secrets never get committed — a fresh clone only has the template.
Fix. Copy the example and fill it in, then set your own team/bundle ID:
cp ShipThatApp/Support/Config.Example.xcconfig ShipThatApp/Support/Config.xcconfig
Bundle identifier, team, and signing live in the target's Signing & Capabilities tab — see Xcode Rename for renaming the project end-to-end.
Config.xcconfig & secrets
This is the single most common source of runtime failures. The app reads every secret from Config.xcconfig (surfaced through Info.plist), so a missing or placeholder value fails at runtime, not compile time.
App crashes on launch with "Supabase configuration not found"
Symptom. The app builds, then immediately crashes. Console shows:
Fatal error: Supabase configuration not found in Info.plist
Cause. AuthManager builds its SupabaseClient eagerly and fatalErrors if SUPABASE_URL or SUPABASE_KEY is missing or empty — by design, so you can't ship an app with broken auth:
let client: SupabaseClient = {
guard let supabaseURLString = Bundle.main.infoDictionary?["SUPABASE_URL"] as? String,
let supabaseURL = URL(string: supabaseURLString),
let supabaseKey = Bundle.main.infoDictionary?["SUPABASE_KEY"] as? String else {
fatalError("Supabase configuration not found in Info.plist")
}
return SupabaseClient(supabaseURL: supabaseURL, supabaseKey: supabaseKey)
}()
Fix. In Config.xcconfig, set both values (no quotes, no https:// scheme on the URL — xcconfig treats // as a comment):
SUPABASE_URL = your-project.supabase.co
SUPABASE_KEY = YOUR_SUPABASE_ANON_KEY
Use the anon/publishable key here, never the service-role key. See Authentication Flow and AuthManager.
Don't quote or scheme your xcconfig values
xcconfig files are not Swift. Wrapping a value in quotes makes the quotes part of the string, and a // (as in https://) starts a comment and silently truncates the value. SUPABASE_URL is stored host-only on purpose.
Analytics or paywalls quietly do nothing
Symptom. No TelemetryDeck events arrive, or paywalls show no products — but the app doesn't crash.
Cause. Unlike auth, the analytics and purchases bootstraps fail soft: a missing key just skips configuration with a guard … else { return } rather than crashing.
guard let telemetryDeckAppID = Bundle.main.infoDictionary?["TD_APP_ID"] as? String else { return }
// ...
guard let revenueCatApiString = Bundle.main.infoDictionary?["RC_API_KEY"] as? String else { return }
Purchases.configure(withAPIKey: revenueCatApiString)
Fix. Fill in TD_APP_ID and RC_API_KEY in Config.xcconfig. Because these fail silently, a placeholder value looks identical to a real one until you check whether events/products actually appear. See Analytics and In-App Purchases.
The signed AI proxy (Chat, DALL·E, Vision, Dex)
Every cloud AI call routes through your backend proxy so your OpenAI key never ships in the app. Requests are HMAC-signed, and there's a per-session token bootstrap that has to succeed first. When AI features fail with auth errors, this is almost always why.
How the signing actually works
On launch, LandingView calls AuthService.authenticate(), which hits GET /auth_token. That single request is signed with your app-embedded API_AUTH_KEY; the backend returns a short-lived session token that's stored in the keychain:
func authenticate() async throws -> AuthResponse {
let result = await ApiClient.shared.sendRequest(
endpoint: Endpoints.auth,
responseModel: AuthResponse.self
)
switch result {
case .success(let authResponse):
saveToKeychain(authResponse.value)
return authResponse
case .failure(let failure):
Logger.viewCycle.fault("Authentication failed")
throw failure
}
}
From then on, every other endpoint signs with that per-session token, not the bootstrap key:
var signingKey: String? {
switch self {
case .auth:
return Config.Api.authKey // app-embedded bootstrap key
case .chatgptSimple, .chatgpt, .dalle, /* ... */ .vision:
return KeychainSwift().get(Config.Keychain.tokenKey) // per-session token
}
}
ApiClient then signs each request over a canonical string — METHOD\nPATH\nTIMESTAMP\nSHA256(body) — and attaches x-signature + x-timestamp headers. The backend recomputes the identical string, so a captured request can't be replayed or tampered with:
let timestamp = String(Int(Date().timeIntervalSince1970))
let bodyHash = CryptoUtils.shared.sha256Hex(body ?? Data())
let canonical = "\(endpoint.method.rawValue)\n\(signedPath)\n\(timestamp)\n\(bodyHash)"
let signature = CryptoUtils.shared.createHmac(key: signingKey, phrase: canonical)
request.setValue(signature, forHTTPHeaderField: "x-signature")
request.setValue(timestamp, forHTTPHeaderField: "x-timestamp")
All AI calls fail with 401 / "Authorization failure"
Symptom. Chat, image generation, Vision, and the Dex scanner all error. Logs show Authentication failed, a 401, then a single retry, then RequestError.unAuthorized.
Cause. The most common reason: API_AUTH_KEY (app) ≠ AUTH_SECRET_KEY (backend). The bootstrap signature won't verify, /auth_token returns 401, no session token gets stored, and every downstream signed call then fails too. ApiClient retries a 401 exactly once after a short delay before giving up:
case 401:
if allowRetry {
try? await Task.sleep(for: .seconds(Double(Config.Api.retryDelay)))
return await sendRequest(/* ...allowRetry: false */)
}
return .failure(.unAuthorized(code: decodedError.code))
Fix. Make the two secrets identical:
# Config.xcconfig (the app)
# Shared HMAC bootstrap secret; must equal the backend AUTH_SECRET_KEY.
API_AUTH_KEY = YOUR_BACKEND_AUTH_SECRET_KEY
# backend env (the Express proxy on Vercel)
AUTH_SECRET_KEY=YOUR_BACKEND_AUTH_SECRET_KEY
Redeploy the backend after changing it, then relaunch the app so authenticate() re-runs and re-mints the session token.
Clock skew breaks HMAC too
The signature embeds a Unix x-timestamp, and the backend rejects stale requests. If the device or simulator clock is wildly off (a common simulator quirk), valid requests look like replays and get rejected. Reset the simulator's time / set the device clock to automatic if signing fails despite matching keys.
AI calls fail but auth/payments work
Symptom. Sign-in and purchases are fine, but only the AI endpoints error.
Cause. Your backend proxy isn't deployed, isn't reachable at Config.Api.baseURL (https://api.shipthat.app/), or its OpenAI key/credits are exhausted. The proxy is what holds the OpenAI key — the app never has it.
Fix.
- Confirm the proxy is deployed and the base URL in
Config.Api.baseURLpoints at your deployment. - Check the backend logs for the failing endpoint (
/chatgpt,/dalle,/vision). - Verify the OpenAI key configured on the backend is valid and funded.
See AI Chat, AI Image Generation, and AI Vision.
RevenueCat & StoreKit
Paywalls load but show no products (or "products not available")
Symptom. The paywall renders its layout but the product rows are empty; PurchaseManager.products stays empty.
Cause. PurchaseManager loads products by exact product ID via StoreKit:
private let productIds = Config.Purchases.productIds // ["sta_999_1m_1w0", "sta_499_1w"]
// ...
self.products = try await Product.products(for: self.productIds)
If those IDs don't exist (or aren't "Ready to Submit") in App Store Connect, Product.products(for:) returns nothing and the paywall has nothing to show.
Fix.
- Create matching subscription products in App Store Connect, or change
Config.Purchases.productIdsto your IDs. The kit also ships asubGroupId(21397077) — update it to your subscription group. - For local iteration without App Store Connect, run against the bundled StoreKit Configuration file (Edit Scheme → Run → Options → StoreKit Configuration).
- Sandbox products can take time to propagate after creation; give it a few minutes.
See Purchase Manager and In-App Purchases.
Sandbox purchases fail or loop
Symptom. Purchases error out, ask to sign in repeatedly, or never complete in the simulator.
Cause. Real sandbox transactions require a physical device signed into a Sandbox Apple Account (Settings → Developer → Sandbox Account), not your normal Apple ID. The simulator can't process real sandbox StoreKit.
Fix. Test purchase flows against the StoreKit Configuration file in the simulator, and validate real sandbox transactions on a device with a dedicated sandbox tester account.
Supabase, Magic Link & Sign in with Apple
Magic Link email never opens the app (or opens Safari and stalls)
Symptom. The Magic Link email arrives, but tapping it opens the browser and the app never gets the session.
Cause. Magic Link and email sign-up both redirect to the app's custom scheme, shipthatapp://login-callback:
try await client.auth.signInWithOTP(
email: email,
redirectTo: URL(string: "shipthatapp://login-callback")
)
For iOS to hand that URL back to the app, the scheme must be registered and allow-listed in your Supabase project.
Fix.
- Confirm the
shipthatappURL scheme is registered (it is, underCFBundleURLSchemesinInfo.plist) — keep it, or rename it consistently everywhere if you change it. - In the Supabase dashboard → Authentication → URL Configuration, add
shipthatapp://login-callbackto the allowed Redirect URLs. - The incoming URL is handled by
.onOpenURLinLandingView, which forwards it togetSessionFromUrl(url:). See Authentication Flow.
Sign in with Apple fails or returns no session
Symptom. The Apple sheet appears but sign-in throws, or no session comes back.
Cause. Two things must be in place: the Sign in with Apple capability on the app target, and Apple configured as a provider in Supabase. The kit verifies a nonce to prevent token replay, so a mismatched/missing nonce also fails:
// The raw nonce is matched against the hashed nonce embedded in the Apple
// identity token, preventing token replay.
let session = try await client.auth.signInWithIdToken(
credentials: .init(provider: .apple, idToken: idToken, nonce: nonce)
)
Fix.
- Add the Sign in with Apple capability in Signing & Capabilities (the kit ships
ShipThatApp.entitlements). - In Supabase → Authentication → Providers → Apple, enable Apple and add your Services ID / key configuration.
- Test on a real device signed into iCloud — Sign in with Apple is unreliable on the simulator.
See Authentication Flow and the Sign In View.
Account deletion fails
Symptom. "Couldn't delete account" toast; deletion throws.
Cause. Deleting an auth user needs the service-role key, which must never ship in the app. The kit invokes a Supabase Edge Function (delete-account) that runs the deletion server-side with admin rights:
try await client.functions.invoke(functionName: "delete-account")
Fix. Deploy the delete-account Edge Function from the backend repo and confirm it has the service-role key in its environment. (This flow exists because App Store Guideline 5.1.1(v) requires in-app account deletion for any app offering account creation.)
SwiftData stores
Crash on launch: "Failed to initialize Dex ModelContainer"
Symptom. The app crashes when first opening the Dex, with a fatal error about a ModelContainer.
Cause. The Dex scanner uses its own isolated SwiftData store (Dex.store) so its schema can never collide with the chat store (ChatMessage). If that container can't be created, DexStore fatalErrors rather than running with a broken store.
Fix.
- After changing a
@Model's schema (adding/removing/retyping a property onDexEntryorChatMessage), a lightweight migration may not be inferable. During development, the simplest fix is to delete the app from the simulator/device and relaunch so the store is recreated. - Don't point
DexStoreat the app's default container or attach a second container forDexEntryelsewhere — keep all Dex reads/writes flowing throughDexStore.shared.container. The chat history store (ChatMessage) is separate by design.
Two stores, on purpose
The Dex and the chat history live in separate SwiftData stores so their schemas evolve independently. If you add your own @Model, decide deliberately which store it belongs to rather than mixing models into one container.
Camera & permissions
Dex scanner crashes when you tap "Scan" (no permission prompt)
Symptom. Opening the camera to scan crashes the app instead of asking for camera access.
Cause. iOS terminates any app that accesses the camera without an NSCameraUsageDescription string. The kit sets this via build settings, not a raw plist key:
INFOPLIST_KEY_NSCameraUsageDescription = "We need access to your camera to take a pic";
Fix. Confirm that key is present in the target's build settings (search "Privacy - Camera Usage Description" in the Info tab) and customize the copy for your app. If you previously denied camera access, reset it: Settings → your app → Camera, or Settings → General → Reset → Reset Location & Privacy on the simulator.
See Dex Scanner for the full scan flow.
Still stuck?
If you've worked through the relevant section and the issue persists:
- Re-read Installation & Setup and Configurations — a single stale
YOUR_…value inConfig.xcconfigcauses a surprising range of symptoms. - Check the Xcode console. The kit logs every failure through
OSLog(Logger.viewCycle,Logger.dex) with context before showing a user-facing Toast — the real cause is almost always in the log line just above the toast. - Confirm the backend proxy is deployed and reachable; AI failures with healthy auth/payments are nearly always a backend or OpenAI-key problem, not an app bug.