Going Live
Shipping to the App Store
The gap between "it runs on my simulator" and "it's live on the App Store" is where most indie projects stall — not because the code is wrong, but because the release work is a maze of consoles, plists, and review rules nobody documents in one place. ShipThatApp already ships the hard, App-Store-specific glue (Sign in with Apple with a nonce, in-app account deletion, an HMAC-signed proxy so your OpenAI keys never leave your backend). This page is the rest of the map: a linear, copy-as-you-go checklist that takes a fresh clone all the way to a submitted build.
Work top to bottom. Each section ends in a concrete, verifiable state.
Prefer the command line?
Everything below is the manual, GUI-driven path through Xcode and App Store Connect. Once you've done it once, you can automate the whole build-and-submit flow from your terminal — version bumps, archiving, uploads, TestFlight, metadata, and the final submission — with the asc CLI. Start at Releasing with the asc CLI; it maps one-to-one onto these same steps.
What you'll need accounts for
A paid Apple Developer Program membership ($99/yr), a Supabase project, a RevenueCat account, an OpenAI key, and a host for the backend proxy (the kit targets Vercel). Optionally TelemetryDeck (analytics) and OneSignal (push). You can ship without analytics and push — but not without the first four.
The Ship-It Checklist at a Glance
- Rename the project, bundle identifier, and URL scheme to your own.
- Move every secret into a production
Config.xcconfig(never committed). - Stand up production Supabase (auth + the
delete-accountEdge Function). - Stand up production RevenueCat (App Store Connect products → RevenueCat entitlements).
- Deploy the AI backend proxy in production and point the app at it.
- Add the camera usage string and fill out App Privacy in App Store Connect.
- Flip entitlements to production (push environment, associated domains, app group).
- Archive → upload → TestFlight → submit.
- Clear the common review traps before you hit Submit.
1. Rename to Your Own App
The template ships under the placeholder identity app.shipthat.* with the URL scheme shipthatapp. Make it yours first — everything downstream (signing, push, deep links) keys off the bundle identifier.
- Project + scheme + target name — follow the step-by-step in Rename Xcode Project.
- Bundle identifier — in Signing & Capabilities, change
app.shipthat.demoto your reverse-DNS id (e.g.com.yourcompany.yourapp). Register the same id under Certificates, Identifiers & Profiles in the Apple Developer portal. - URL scheme — the auth deep link uses the scheme
shipthatapp. Pick your own and change it in both places it appears, or Magic Link / email confirmation callbacks won't return to the app:
ShipThatApp/Info.plist → CFBundleURLTypes → CFBundleURLSchemes
Services/Auth/AuthManager.swift → the redirectTo URLs (e.g. yourapp://login-callback)
The scheme has to match in three places
The custom scheme lives in Info.plist (CFBundleURLTypes), in the redirectTo URLs inside AuthManager, and in your Supabase project's Authentication → URL Configuration allow-list. If any of the three disagree, the link will open the wrong app — or none. See Authentication for the full deep-link flow.
2. Production Secrets in Config.xcconfig
ShipThatApp is security-first by design: no key is ever hardcoded in a Swift file. Secrets live only in ShipThatApp/Support/Config.xcconfig, which is gitignored. They're injected at build time into Info.plist and read back via Bundle.main.infoDictionary — so production keys never touch source control, screenshots, or your git history.
If you haven't already, create the file from the tracked example:
cp ShipThatApp/Support/Config.Example.xcconfig ShipThatApp/Support/Config.xcconfig
Fill it with your production values:
TD_APP_ID = your-telemetrydeck-app-id
RC_API_KEY = your-revenuecat-PUBLIC-sdk-key
ONESIGNAL_APP_ID = your-onesignal-app-id
SUPABASE_URL = your-project.supabase.co
SUPABASE_KEY = your-supabase-ANON-key
API_AUTH_KEY = your-backend-AUTH_SECRET_KEY
A few rules that keep you out of trouble:
- Use only the public RevenueCat SDK key (
appl_…), never a secret API key. - Use the Supabase anon key, never the service-role key — the only privileged operation (account deletion) runs server-side in an Edge Function.
API_AUTH_KEYmust byte-for-byte equal the backend'sAUTH_SECRET_KEY. It's the shared secret that bootstraps the HMAC-signed proxy; a mismatch means every AI call gets rejected.- No quotes around xcconfig values — that's a silent footgun in the
.xcconfigformat.
Verify the gitignore before your first commit
Run git check-ignore ShipThatApp/Support/Config.xcconfig — it should print the path (meaning it's ignored). If it prints nothing, your secrets are about to be committed. Only Config.Example.xcconfig (the placeholder template) belongs in git. Full key reference: Configuration & Secrets.
3. Production Supabase
Auth is already wired through a single @Observable AuthManager (see Authentication). For production you need a real project and one deployed function.
- Create a production Supabase project and copy its URL and anon key into
Config.xcconfig(step 2). - Add your redirect URL under Authentication → URL Configuration. It must match your custom scheme, e.g.
yourapp://login-callback. Without it, Magic Link and email-confirmation links fail silently. - Enable Sign in with Apple under Authentication → Providers and paste in your Apple Services ID / key. The client side (nonce generation, token exchange) is already done.
- Deploy the
delete-accountEdge Function. In-app account deletion (App Store Guideline 5.1.1(v)) calls a Supabase Edge Function so the service-role key never ships in the binary:
func deleteAccount() async throws {
try await client.functions.invoke(functionName: "delete-account")
try? await client.auth.signOut()
// ... clears cached profile + Keychain token, then currentSession = nil
}
The function source lives in the backend repo's supabase/functions/. Deploy it with the Supabase CLI:
supabase functions deploy delete-account
Test deletion against production before submitting
Reviewers actively test account deletion. Create a throwaway account in your production project, delete it from Settings → Delete Account, and confirm the user is gone from the Supabase dashboard. A deletion button that only signs out is an automatic rejection.
4. Production RevenueCat & App Store Connect Products
Purchases run through RevenueCat on top of StoreKit (see In-App Purchases). The template references two product ids in Config.swift:
enum Purchases {
static let productIds = ["sta_999_1m_1w0", "sta_499_1w"]
static let subGroupId = "21397077"
static let minAppRunsBeforeReviewReq = 4
}
Those are demo ids. Replace them end to end:
- App Store Connect → your app → Subscriptions. Create a subscription group, then your subscription products. Set price, duration, and any intro offer. Note each product id and the new group id.
- Update
Config.swift— setproductIdsto your ids andsubGroupIdto your group id. (These are non-secret build constants, so they live inConfig.swift, notConfig.xcconfig.) - RevenueCat dashboard — create your app, add the App Store as a platform, and upload your App Store Connect API key so RevenueCat can validate receipts.
- Entitlements & Offerings — define an entitlement (e.g.
pro), attach your products to it, and group them into an Offering.PurchaseManager.hasUnlockedProgates Pro features off the active entitlement. - Paste the RevenueCat public SDK key into
Config.xcconfigasRC_API_KEY.ShipThatAppAppconfigures the SDK at launch:
private func configureRevenueCat() {
guard let revenueCatApiString = Bundle.main.infoDictionary?["RC_API_KEY"] as? String else { return }
Purchases.configure(withAPIKey: revenueCatApiString)
}
Turn off RevenueCat debug logging for release
ShipThatAppApp.configureRevenueCat() sets Purchases.logLevel = .debug. That's great while developing and noisy in production — drop it to .warn (or wrap it in #if DEBUG) before you archive.
Restore Purchases is required
Apple requires a visible Restore Purchases control on any screen that sells subscriptions. The kit's Paywall3View already implements it with AppStore.sync():
Button {
Task {
do {
TelemetryManager.send("clickedRestorePurchase")
try await AppStore.sync()
} catch {
Logger.viewCycle.error("Restore error: \(error.localizedDescription)")
}
}
} label: {
Text("Restore Purchases")
}
If you ship Paywall1View or Paywall2View instead, copy this Restore button onto them — only Paywall3View has it today, and a paywall without a restore option is a guaranteed rejection.
5. AI Backend Proxy in Production
This is the kit's headline security feature, and it matters most at ship time. The app never holds your OpenAI key. Every cloud AI call — ChatGPT, DALL·E, Vision, and the Dex Scanner — goes through ApiClient.shared.sendRequest, which HMAC-signs each request before it leaves the device. The proxy validates the signature, calls OpenAI with the key that lives only on your server, and returns the result.
To go live:
- Deploy the backend (
ship-that-app-backend, an Express proxy) to production — the kit targets Vercel. Set its env vars there: your realOPENAI_API_KEYand anAUTH_SECRET_KEY. - Match the shared secret.
API_AUTH_KEYinConfig.xcconfigmust equal the backend'sAUTH_SECRET_KEY. This is the bootstrap secret for request signing. - Point the app at production.
Config.swiftdefaults the base URL to the hosted endpoint:
enum Api {
static let baseURL = "https://api.shipthat.app/"
static let authKey = Bundle.main.infoDictionary?["API_AUTH_KEY"] as? String ?? ""
}
Change baseURL to your deployed proxy's URL.
Set OpenAI usage limits before launch
Because the proxy holds the only copy of your key, a viral launch (or abuse) hits your bill. The backend already does HMAC signing and rate limiting — but also set a hard monthly spend cap in the OpenAI dashboard as a backstop.
6. Camera Permission & App Privacy
Add the camera usage string
The Dex Scanner and the AI Vision feature both open the camera (ImagePicker with sourceType: .camera). iOS will crash the app the instant the camera is invoked if there's no usage-description string — and App Review will reject a build that requests the camera without one.
The template's Info.plist does not ship this key yet, so add it before you archive. In Xcode, select the target → Info tab → add a row, or edit Info.plist directly:
Key: Privacy - Camera Usage Description (NSCameraUsageDescription)
Value: Used to photograph items so you can identify and save them to your collection.
Write a specific reason. "We need the camera" is a rejection; describing the actual feature is not.
Fill out the App Privacy nutrition label
The kit ships a PrivacyInfo.xcprivacy manifest that declares what the app touches:
- Email address — collected, linked to the user, used for App Functionality (auth), not used for tracking.
- UserDefaults access — declared with the required reason code
CA92.1.
In App Store Connect → App Privacy, mirror this manifest, then add anything your integrations collect that the bundled manifest doesn't already cover:
- Email Address — Account/auth (already in the manifest).
- Usage Data / Product Interaction — if you keep TelemetryDeck analytics.
- Photos / Camera — the camera images you send to the Vision proxy. Declare how you use them; if you don't retain them server-side, say so.
- Purchase History — RevenueCat subscription state.
Audit your third-party SDKs' manifests
RevenueCat, Supabase, OneSignal, and TelemetryDeck each ship their own privacy manifests inside their SwiftPM packages. Xcode aggregates them into the final Privacy Report at archive time. Generate it (Organizer → your archive → Generate Privacy Report) and make sure your App Store Connect answers match what that report shows.
7. Flip Entitlements to Production
The bundled ShipThatApp.entitlements is configured for development. Before archiving, confirm each capability points at production:
- Push notifications —
aps-environmentisdevelopmentin the template. Xcode swaps this toproductionautomatically in a Release archive only if the Push Notifications capability is enabled for your bundle id in the Developer portal. Verify it. (Skip entirely if you're not shipping OneSignal push.) - Sign in with Apple —
com.apple.developer.applesigninis already set toDefault; just confirm the capability is enabled for your app id. - Associated Domains —
applinks:app.shipthatis a placeholder. Change it to your own domain (or remove it if you're not using universal links). - App Group —
group.app.shipthat.demo.onesignalis used by the OneSignal Notification Service Extension. Rename it to match your bundle id (e.g.group.com.yourcompany.yourapp.onesignal) and create the matching group in the Developer portal, or drop the extension if you're not using push.
8. Archive, TestFlight, Submit
With signing set to your team and a Release configuration selected:
- Bump the version & build number. Set the marketing version (e.g.
1.0.0) and an increasing build number in the target's General tab. Every upload needs a unique build number. - Select "Any iOS Device (arm64)" as the run destination — you can't archive against a simulator.
- Product → Archive. When it finishes, the Organizer opens.
- Validate App first — it catches missing icons, entitlement mismatches, and bad provisioning before you waste an upload.
- Distribute App → App Store Connect → Upload.
- The build appears in App Store Connect after processing (a few minutes to an hour). Add it to TestFlight, test the full flow on a real device — sign in, purchase in the sandbox, scan with the camera, delete the account — then attach the build to your App Store version and Submit for Review.
Any iOS Device (arm64) → Product ▸ Archive → Validate → Distribute ▸ App Store Connect
Test purchases without spending real money
Create a Sandbox tester in App Store Connect (Users and Access → Sandbox) and sign into it on your test device under Settings → Developer. TestFlight builds run StoreKit in the sandbox, so you can exercise the whole RevenueCat purchase + restore flow for free.
9. Common Review Traps — Clear These Before Submitting
These are the rejections that hit boilerplate-based apps most often. The kit handles several for you; confirm each is actually wired for your configuration.
- Account deletion (Guideline 5.1.1(v)). Required for any app with accounts. Handled in Settings → Delete Account →
AuthManager.deleteAccount()→ the Supabase Edge Function. Confirm the function is deployed to production (step 3). - Restore Purchases. Required on every paywall. Present on
Paywall3View; add it to whichever paywall you ship (step 4). - Camera permission string. Missing in the template — add
NSCameraUsageDescriptionwith a specific reason or the app is rejected (and crashes) the first time the scanner opens (step 6). - Accurate App Privacy answers. Your App Store Connect privacy answers must match the bundled
PrivacyInfo.xcprivacyand the aggregated Privacy Report (step 6). - Sign in with Apple offered alongside other logins. If you offer third-party login (e.g. Google), App Store rules require Sign in with Apple too. The kit already includes it (see Authentication).
- Working demo credentials. If anything sits behind a login wall, provide a working test account in App Review Information, or include a Sign in with Apple path the reviewer can complete.
- No debug logging or placeholder content. Drop
Purchases.logLevelto.warn, remove any sample copy, and make sure no screen still shows theapp.shipthatplaceholder identity. - Functional subscriptions in review. Your subscription products must be in the "Ready to Submit" state and attached to the same version you submit, or the reviewer sees an empty paywall.
Submit subscriptions with the build, not after
A frequent first-submission stumble: the products sit in "Missing Metadata" and aren't selected on the version page. In App Store Connect, on the version you're submitting, scroll to In-App Purchases and Subscriptions and tick the products so they're reviewed together with the build.
Related
- Rename Xcode Project — the project/bundle-id rename this guide builds on.
- Configuration & Secrets — every key in
Config.xcconfigandConfig.swift. - Authentication — Sign in with Apple, deep-link redirects, and account deletion.
- In-App Purchases — RevenueCat, paywalls, and the restore flow.
- Dex Scanner — the camera-driven AI feature that needs
NSCameraUsageDescription. - Releasing with the asc CLI — automate this whole flow from your terminal: auth, the command spine, and how it maps onto these steps.
- Build, Upload & TestFlight with asc — version bumps, archive, export, upload, and TestFlight from the command line.
- Metadata & Submitting with asc — manage listing metadata and drive the final submission with
asc publish.