Production Launch Checklist
Civix — iOS App Store Launch Checklist
App: Civix (US Citizenship Test Prep) · ASC App ID 6759538146 · Apple Team 22GBQ3S2M7
Web API: https://api.civixapp.us (Vercel, root dir apps/web)
Entitlement: premium · Monthly: civix_pro ($9.99 USD fallback) · Annual: civix_pro_annual ($59.99 USD fallback)
Generated by the launch-readiness review (2026-06-24). The code is GO; the only true blockers are env/config items below.
0. Verified status — 2026-08-05
Read against the live Vercel project and the RevenueCat API (via MCP), not assumed.
The RevenueCat catalogue was EMPTY, and is now built
Project proj2f6e7224 ("Civic-af") had zero products, zero entitlements and
zero offerings — only an app record. Nobody caught it because the failure is
silent by design: getPurchasablePackageIds() returns empty and the paywall
declines to show a plan rather than showing a dead buy button, which looks
identical to a slow network. The "4 Active Customers, $0 MRR" dashboard reading
was not a pricing problem; there had never been anything to buy.
Built and read back on 2026-08-05:
| Object | Identifier | Wiring | RevenueCat ID |
|---|---|---|---|
| Entitlement | premium | both products attached | entl9f798af95b |
| Product | civix_pro | subscription · App Store | prodf9eb4357f2 |
| Product | civix_pro_annual | subscription · App Store | prod6229c85185 |
| Offering | default | is_current: true | ofrngc2222def9f |
| Package | $rc_monthly | → civix_pro | pkgef9c1e82248 |
| Package | $rc_annual | → civix_pro_annual | pkge22b3c6e56d |
App record: app15e2e8a57b (App Store, bundle com.civics.test).
Checked against the code rather than assumed: lib/purchases.ts matches on
p.product.identifier === PRODUCT_IDS[id], so the store identifiers are the
join key — package lookup_keys are cosmetic. fetchOfferings() reads
offerings.current only, which is why is_current is the load-bearing flag.
Necessary, not sufficient. A RevenueCat product is a pointer at a store SKU. Until App Store Connect has subscriptions with these exact identifiers, StoreKit resolves nothing,
availablePackagesis empty, and the paywall still cannot sell. See §4.
Still open on the RevenueCat side
- No subscriber attributes are set and
logIn/logOutare never called (Purchases.configureruns anonymous), so revenue cannot be segmented by language, interview mode, or engagement. Free to add, no SDK needed.
Live App Store product state — read 2026-08-06 02:07 UTC
The App Store Connect API key is now configured, so get-product-store-state
returns Apple's own view. Both products report
store_status: needs_action / MISSING_METADATA, which is why StoreKit still
resolves nothing:
civix_pro | civix_pro_annual | |
|---|---|---|
| Duration | ONE_MONTH ✅ | ONE_YEAR ✅ |
| US price | $9.99 ✅ | $59.99 ✅ |
| Worldwide pricing | all territories ✅ | all territories ✅ |
| Subscription group | Civix Premium ✅ | Civix Premium ✅ |
| Privacy policy URL | set ✅ | set ✅ |
| Localization | en-US present ✅ | {} empty ❌ |
| Review notes | present ✅ | null ❌ |
| Review screenshot | absent ❌ | absent ❌ |
So the only remaining work is: a review screenshot on both, plus localization and review notes on the annual.
Availability is US-only on both — availability.territories contains just
US, and available_in_new_territories: false. That is coherent for a
naturalization app (applicants are US residents, and App Store territory
follows the account's country rather than physical location), but the app's own
territory availability must match. If the app ships worldwide while the
products do not, non-US accounts get a paywall they cannot buy from and the
code will correctly refuse to show a plan.
Confirmed working
- RevenueCat iOS SDK is live: the dashboard shows real Active Customers, which
means
EXPO_PUBLIC_REVENUECAT_IOS_API_KEYineas.jsonconfigures successfully on device. - Production API healthy: OpenAI configured, Upstash Redis connected, rate
limiting active in
redismode, session store ready. gpt-realtime-2.1is still OpenAI's recommended voice-agent model and is the configured default. The GA migration is complete.
Confirmed missing on Vercel production — each one costs money
REVENUECAT_SECRET_API_KEY— absent. Every paying subscriber is currently 403'd out of the realtime interview. Verify after setting with:curl -s https://api.civixapp.us/api/health | grep entitlementVerificationOPENAI_VOICE_BUDGET_MONTHLY_USD— absent due to a typo: the variable is namedPENAI_VOICE_BUDGET_MONTHLY_USD(missing leadingO), soreserveVoiceBudget()returnsallowed: trueunconditionally. Add the correct name and remove the typo'd one.OPENAI_BUDGET_MONTHLY_USDandOPENAI_DAILY_REQUEST_CAP— absent, soenforceOpenAIBudget()returns immediately and there is no dollar ceiling on/chat,/evaluate,/explain,/voice.TESTFLIGHT_BYPASS_ENABLEDis set on Production. Disable it for the public launch window.
These caps are not hardening — they are the only spend defense this build has. Rate limiting keys on the client-supplied
x-device-idheader, so rotating it voids every per-identifier limit; and the HMAC layer cannot be enabled because the mobile client contains no request-signing code (INTERVIEW_SECURITY_MODE=enforcewould 401 every real user). Client-side signing is the next build's work.
Paywall claims — all five re-verified against the code that enforces them. Two were false and are fixed: free users get 3 AI explanations/day (not zero) and the full adaptive engine (not none).
Free trial — the paywall is now trial-aware. It reads the live introductory offer from the RevenueCat product, checks eligibility per Apple ID, and renders the Guideline 3.1.2 disclosure. It is inert until an Introductory Offer exists in App Store Connect; create one and it activates with an OTA update, no rebuild.
1. Mobile env / EAS
-
EXPO_PUBLIC_REVENUECAT_IOS_API_KEYis set in EAS for the production profile (EAS Secret / project env). Without it RevenueCat returns no offerings → paywall shows "products still loading" and purchases never complete. -
EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEYpresent too (only relevant if shipping Play in parallel). - Confirm the public build uses the
productionEAS profile — NOTtestflight. Thetestflightprofile setsEXPO_PUBLIC_ENABLE_TEST_BYPASS=trueandEXPO_PUBLIC_TESTFLIGHT_INTERVIEW_BYPASS=true; these MUST stay out of the App Store build so test bypasses are disabled. -
productionprofile setsEXPO_PUBLIC_API_URL=https://api.civixapp.us(verify ineas.json, no trailing slash). -
appVersionSource: "remote"+autoIncrement: trueonproduction— confirm no build-number collision with the last TestFlight build. - Native-change builds are NOT OTA. Any change to native modules (
civics-activity,civics-haptics,civics-realtime-audio,civics-activity-grid,civics-hero-card,civics-answer-choice,civics-officer-stage), the widget extension, pods, orInfo.plistrequires a fresheas build+ App Store submit. - Do NOT run
npm run prebuild— it is disabled and would wipe the committedios/native code.
2. Vercel production env (then redeploy)
-
REVENUECAT_SECRET_API_KEY— CRITICAL.lib/interview/entitlement.tsFAILS CLOSED when this is unset: real paying subscribers get 403'd out of the realtime interview in production. Set the RevenueCat secret (server) key, not the public SDK key. -
OPENAI_API_KEY— required for all AI interview / evaluate / realtime-token routes. -
OPENAI_REALTIME_PROMPT_ID— set to the published prompt id. Diff the dashboard-saved prompt to confirm it still carries: (a) the language-routing rules and (b) the no-real-personal-data / synthetic-identity rule. (Code injects these guardrails on connect too, but the saved prompt should still carry them.) -
UPSTASH_REDIS_URL+UPSTASH_REDIS_TOKEN— without these the rate limiter / OpenAI cost caps fall back to in-memory and become per-serverless-instance, so caps multiply across instances and stop protecting spend. -
OPENAI_BUDGET_MONTHLY_USD+OPENAI_DAILY_REQUEST_CAP+OPENAI_VOICE_BUDGET_MONTHLY_USD— the only spend ceiling that exists. Unset meansenforceOpenAIBudget()andreserveVoiceBudget()no-op and OpenAI spend is unbounded. Watch for thePENAI_…typo noted in §0. - TestFlight bypass vars —
TESTFLIGHT_BYPASS_ENABLED+TESTFLIGHT_BYPASS_SECRET(mobile:EXPO_PUBLIC_TESTFLIGHT_BYPASS_TOKEN). TestFlight-only; must NOT gate the public production entitlement path. Currently enabled on Production — disable for the public launch window. - Redeploy the Vercel production deployment after changing any env var. Then
cd apps/web && npm run verify-apiandnpm run test:interview-apiagainst prod.
3. RevenueCat dashboard
-
premiumentitlement exists and is attached to bothcivix_proandcivix_pro_annual. (built 2026-08-05, §0) - Both products are registered in RevenueCat against app
app15e2e8a57b. (built 2026-08-05 — note these are pointers; the ASC SKUs in §4 must still exist) - The current Offering is marked default/current and contains monthly and annual packages (paywall reads localized prices live from
offerings.currentand defaults to annual). (built 2026-08-05,is_current: trueverified) - iOS App Store shared secret / App Store Connect API key configured in RevenueCat so server-side entitlement verification resolves real receipts. Currently
app_store_connect_api_key_configured: false. - Sandbox purchase resolves the
premiumentitlement end-to-end. - When testing purchases from TestFlight, turn Sandbox data ON in the RevenueCat dashboard — sandbox transactions are hidden by default and a successful test purchase will look like it failed.
- Optional: create an Introductory Offer (free trial) on
civix_pro_annualin App Store Connect. The paywall detects it automatically — offer marker, trial CTA, and the required "converts to a paid subscription at …" disclosure.
4. App Store Connect
This section is now the critical path. As of 2026-08-05 the RevenueCat side is complete (§0) and these SKUs are the only thing standing between the build and a paywall that can take money. Apple reviews subscriptions alongside the binary, so they cannot be added after submission.
- Paid Applications Agreement signed and active, with tax and banking complete (IAP will not work otherwise — this alone blocks many first launches).
- Confirm whether the account is enrolled in the Small Business Program: 15% vs 30% commission materially changes the margin (see the unit-economics note in §0 of the launch review).
- One subscription group containing both products, so users can move between plans.
-
civix_pro— auto-renewable, 1 month, $9.99. Identifier must match character for character. -
civix_pro_annual— auto-renewable, 1 year, $59.99. Same. - Review screenshot uploaded for each subscription. Easiest to forget, and Apple rejects the product itself without it.
- Both products are Ready to Submit and selected in this version's In-App Purchases section before you hit submit.
- Subscription group, localized display name, and description set.
- App Privacy labels complete: Microphone (AI voice interview), plus analytics/crash (Sentry) data types; declare data use/linking honestly.
- Screenshots for all required device sizes; no placeholder content.
- Age rating questionnaire completed.
- App description / promo text includes the non-affiliation disclaimer ("not affiliated with USCIS or the U.S. Government").
- Privacy Policy URL (
https://civixapp.us/privacy) and Terms (https://civixapp.us/terms) live and reachable. - Export compliance answered.
5. Build & submit
-
eas build --platform ios --profile testflightfor internal QA round (bypass enabled), OR--profile productionfor the App Store candidate. -
Info.plist:CFBundleShortVersionStringuses$(MARKETING_VERSION)(avoid the ITMS-90473 hardcoded-version reject). -
UIRequiresFullScreen = truepresent (prevents the ITMS-90474 iPad multitasking reject). -
NSMicrophoneUsageDescriptionpresent and accurate (voice interview). - Podfile.lock committed and current (any new native module needs
pod installbefore pushing to EAS). - Submit the production-profile build only via
eas submit --platform ios --profile production. Never submit atestflight-profile build to the public track.
6. Pre-submit on-device SMOKE TESTS (most important)
Run on a real device, signed into a Sandbox/TestFlight account. Repeat the audio cases with AND without AirPods/wired headphones.
Realtime voice / audio routing
- Start an AI interview → officer audio plays through device speaker.
- When you begin speaking, audio does NOT switch routes, duck hard, or flap — no cut-out/glitch at the moment the mic engages.
- Connect AirPods mid-session: audio moves to AirPods cleanly and does not flap as you alternate speaking/listening.
- Wired headphones: stable route, no flapping when you start speaking.
- Remove headphones mid-session: falls back to speaker without crashing the session.
- Officer voice (
cedar) is intelligible; no echo/feedback loop on speaker.
Language routing
- Speak Dari/Pashto to the officer → officer redirects you to answer in English (does not conduct the civics exam in the other language).
- No real personal data is solicited/echoed (synthetic identity rule holds).
Entitlement / paywall gates
- Free account: premium feature is blocked — locked questions (64/128), adaptive flow-zone, native-language study, uncapped AI interviews are gated; tapping a locked feature routes to the paywall.
- Paid account (premium): realtime interview is allowed — no 403 (validates
REVENUECAT_SECRET_API_KEYon Vercel is live). - Free weekly AI interview is reachable for a free user.
- 1 free mock interview/day limit is enforced for free users (2nd same-day attempt blocked/paywalled, resets next day).
- Purchase flow completes in Sandbox →
premiumentitlement activates → gated features unlock immediately. - Restore Purchases works on a fresh install / re-login.
- Paywall close (X) dismisses cleanly; "Maybe Later" also dismisses. No trap.
- Auto-renew disclosure + Terms/Privacy links on the paywall open correctly.
UI correctness
- Quiz answer options do not overlap — check across questions with different line counts (1-line vs 3-line options).
- Practice answer options do not overlap — same check, including when advancing between questions of differing heights.
- Consent gate appears before the interview starts and must be accepted to proceed (both AI and mock realtime).
- PDF export of results generates and opens/shares correctly.
- Tab bar restores after the interview ends (all 5 tabs return).
- RTL languages (fa/ps) render correctly on cold start.
7. Post-launch monitoring (Sentry)
Watch breadcrumbs/errors for:
- Realtime: realtime-token 4xx/5xx, audio-engine init failures, session-start errors, route-change exceptions,
stuck_speaking_recovered, OpenAI rate-limit/cost-cap hits. - Entitlement 403s: spikes on the realtime path =
REVENUECAT_SECRET_API_KEYmisconfig or RevenueCat outage (subscribers failing closed). - Purchases: "No offerings available" / "unavailable" errors, purchase failures, restore failures.
- Gates: free-weekly-interview unreachable, daily-mock-limit logic errors, paywall navigation dead-ends.
- Vercel function logs: Upstash fallback warnings, OpenAI 429s, prompt-id resolution failures.
8. Rollback plan
- JS-only / config / copy / prompt-display fix →
eas update --channel production(OTA). Use for paywall copy, JS gate logic, string fixes, non-native bugfixes. - Native fix (any
ios/native module, widget, pod,Info.plist, new permission, SDK bump) → neweas build+ App Store submit. Cannot be OTA'd. - Server-side fix (entitlement/realtime/prompt/rate-limit) → change Vercel env or code, redeploy prod. For a bad realtime prompt, revert
OPENAI_REALTIME_PROMPT_IDto the previous published version. - Emergency revenue/abuse stop → flip the relevant server gate or rotate
OPENAI_API_KEY/ tighten Upstash caps; pull a broken OTA by publishing a known-goodeas update. - Keep the previous known-good build available in App Store Connect to expedite a phased-release halt if a native regression ships.