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:

ObjectIdentifierWiringRevenueCat ID
Entitlementpremiumboth products attachedentl9f798af95b
Productcivix_prosubscription · App Storeprodf9eb4357f2
Productcivix_pro_annualsubscription · App Storeprod6229c85185
Offeringdefaultis_current: trueofrngc2222def9f
Package$rc_monthlycivix_propkgef9c1e82248
Package$rc_annualcivix_pro_annualpkge22b3c6e56d

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, availablePackages is empty, and the paywall still cannot sell. See §4.

Still open on the RevenueCat side

  • No subscriber attributes are set and logIn/logOut are never called (Purchases.configure runs 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_procivix_pro_annual
DurationONE_MONTH ✅ONE_YEAR ✅
US price$9.99 ✅$59.99 ✅
Worldwide pricingall territories ✅all territories ✅
Subscription groupCivix Premium ✅Civix Premium ✅
Privacy policy URLset ✅set ✅
Localizationen-US present ✅{} empty
Review notespresent ✅null
Review screenshotabsentabsent

So the only remaining work is: a review screenshot on both, plus localization and review notes on the annual.

Availability is US-only on bothavailability.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_KEY in eas.json configures successfully on device.
  • Production API healthy: OpenAI configured, Upstash Redis connected, rate limiting active in redis mode, session store ready.
  • gpt-realtime-2.1 is 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_KEYabsent. 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 entitlementVerification
  • OPENAI_VOICE_BUDGET_MONTHLY_USDabsent due to a typo: the variable is named PENAI_VOICE_BUDGET_MONTHLY_USD (missing leading O), so reserveVoiceBudget() returns allowed: true unconditionally. Add the correct name and remove the typo'd one.
  • OPENAI_BUDGET_MONTHLY_USD and OPENAI_DAILY_REQUEST_CAPabsent, so enforceOpenAIBudget() returns immediately and there is no dollar ceiling on /chat, /evaluate, /explain, /voice.
  • TESTFLIGHT_BYPASS_ENABLED is 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-id header, 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=enforce would 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_KEY is 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_KEY present too (only relevant if shipping Play in parallel).
  • Confirm the public build uses the production EAS profile — NOT testflight. The testflight profile sets EXPO_PUBLIC_ENABLE_TEST_BYPASS=true and EXPO_PUBLIC_TESTFLIGHT_INTERVIEW_BYPASS=true; these MUST stay out of the App Store build so test bypasses are disabled.
  • production profile sets EXPO_PUBLIC_API_URL=https://api.civixapp.us (verify in eas.json, no trailing slash).
  • appVersionSource: "remote" + autoIncrement: true on production — 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, or Info.plist requires a fresh eas build + App Store submit.
  • Do NOT run npm run prebuild — it is disabled and would wipe the committed ios/ native code.

2. Vercel production env (then redeploy)

  • REVENUECAT_SECRET_API_KEYCRITICAL. lib/interview/entitlement.ts FAILS 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_USDthe only spend ceiling that exists. Unset means enforceOpenAIBudget() and reserveVoiceBudget() no-op and OpenAI spend is unbounded. Watch for the PENAI_… 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-api and npm run test:interview-api against prod.

3. RevenueCat dashboard

  • premium entitlement exists and is attached to both civix_pro and civix_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.current and defaults to annual). (built 2026-08-05, is_current: true verified)
  • 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 premium entitlement 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_annual in 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 testflight for internal QA round (bypass enabled), OR --profile production for the App Store candidate.
  • Info.plist: CFBundleShortVersionString uses $(MARKETING_VERSION) (avoid the ITMS-90473 hardcoded-version reject).
  • UIRequiresFullScreen = true present (prevents the ITMS-90474 iPad multitasking reject).
  • NSMicrophoneUsageDescription present and accurate (voice interview).
  • Podfile.lock committed and current (any new native module needs pod install before pushing to EAS).
  • Submit the production-profile build only via eas submit --platform ios --profile production. Never submit a testflight-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_KEY on 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 → premium entitlement 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_KEY misconfig 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 fixeas 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) → new eas 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_ID to 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-good eas update.
  • Keep the previous known-good build available in App Store Connect to expedite a phased-release halt if a native regression ships.