Pre-launch Checklist
Status: Active
Detailed checklist for everything that must complete BEFORE the app is publicly listed on App Store / Play Store. Use External Blockers Register for the summary routing view; this file owns the detailed actions, code/data locations, URLs, and risk notes.
Status legend: ⛔ blocked on user/external · ⏳ in progress · ✅ done · ➖ optional
All items in this document are PARKED. They unblock launch but don't unblock engineering work. The team is NOT actively trying to close them right now — they require external services (Apple Developer, Korean counsel, KCC lbsc.kr filing) or user-side info (CPO contact) that the engineering team cannot supply. Engineering focuses on app hardening + features per
docs/production-readiness.md. When external prerequisites land, the engineering steps in each section below are minutes-to-hours of work.
Last reviewed: 2026-06-28 (canonical release-versioning and preview-first OTA workflow documented)
⛔ 1. CPO (Chief Privacy Officer) real contact info — PIPA §30 mandatory
Why: Korean Personal Information Protection Act §30(1)(7) requires every PII-collecting service to publicly designate a CPO with real name, email, and phone — placeholder values are an immediate PIPC violation upon first user signup. The 2024–2026 enforcement trend treats placeholder CPO data as an aggravating factor in penalty calculations (KakaoPay/Apple §28-8 ruling, Jan 2025).
Action:
- Designate a real person internally as 개인정보 보호책임자 (typically the founder or CTO until the team grows; per PIPA Article 31, this person must have legal standing to make privacy decisions on behalf of the company).
- Provision a dedicated
privacy@<domain>email mailbox + a real phone number (mobile is fine). - Replace the placeholders in
packages/features/profile/src/privacy-screen.tsx:254-257:' · 성명: 이성규\n' + ... ' · 이메일: privacy@twomore.app\n' + ' · 전화: 02-0000-0000 (TODO: 실제 연락처로 교체 필요)\n\n' + - Drop the
(TODO: 실제 연락처로 교체 필요)annotation. - Commit + push +
yarn ota:previewwhile the app is pre-production. Production OTA is launch-gated until R4 Launch Readiness is accepted.
Risk if skipped: First user signup triggers PIPA §30 violation. Penalty escalates per user, but the placeholder structure makes the violation per-impression rather than per-collection — every privacy screen view is a separate breach. Range: KRW 5M–30M base + per-user uplift.
⛔ 2. Counsel-approved Korean policy text — PIPA §15 / §17 / §30, 정통망법 §50
Why: Current KR-1.0.0 policy text was assistant-drafted from PIPA + 정통망법 templates. It tracks the regulatory structure but has not been reviewed by Korean counsel. Counsel review catches: (a) idiomatic Korean legal-drafting conventions the LLM doesn't know, (b) hidden disclosures the LLM might miss for the project's specific data flows (cross-border to Supabase US, Sentry US, Expo Push US — all three are §28-8 entrustment cases), (c) edge cases like guardian consent for minors (PIPA §22-2) that interact with the under-14 birth-year gate.
Action:
- Engage a Korean privacy / IT-law counsel (typical retainer: KRW 3-5M for a startup-tier MVP policy review, ~2-3 week turnaround).
- Provide them: current
PrivacyScreenfull text body,TermsScreen, the purpose_catalog seed migration (lists every purpose key + retention + legal basis), and a short ops-flow doc (what data goes where). - Apply edits to both
*-screen.tsxfiles. - Bump
packages/app/src/config/legal/policy-versions.ts:tsterms: { version: 'KR-1.0.1', url: ... }, privacy: { version: 'KR-1.0.1', url: ... }, crossBorder: { version: 'KR-1.0.1', url: ... }, location: { version: 'KR-1.0.1', url: ... }, marketing: { version: 'KR-1.0.1', url: ... }, - Ship a new migration
001XX_policy_versions_kr_1_0_1.sqlthat INSERTs a new row intopublic.policy_versionsfor each affected policy type, with the newcontent_hash. Existing users get re-prompted to consent to the new version next time they grant any consent. - Update
docs/rebuild-log.mdwith the counsel review date + reviewer name (for audit trail).
Risk if skipped: PIPC may treat AI-generated policy text as insufficient under §30's "easy-to-understand" requirement. More concretely: the cross-border transfer section (Supabase/Sentry/Expo Push) is the highest-enforcement-priority area in 2025 — counsel verifies the §28-8 disclosure language is precise enough to survive the Jan-2025 KakaoPay/Apple template.
⛔ 3. 위치기반서비스사업자 신고 at lbsc.kr — KCC mandatory before GPS feature goes live
Why: Location Information Act (위치정보의 보호 및 이용 등에 관한 법률) §9 requires any business that uses GPS for a user-visible feature to file as a 위치기반서비스사업자 with the Korea Communications Commission (KCC) BEFORE launching the feature. The TwoMore app uses GPS in useGpsCheckin (Phase 6 attendance check-in) + indirectly via useGpsDefaultRegion (pickup filter default region) — both trigger this requirement.
There is a 1-month 소상공인 (small business) grace period after launch, but the safer posture is to file before public availability.
Action:
- Register a business entity (사업자등록) if not already done. Sole proprietor (개인사업자) suffices for the filing — corporate (법인) is not required.
- Go to https://lbsc.kr (KCC location-based-services registry).
- Choose 신고 (not 허가 — 허가 is for telco-level GPS providers).
- Required documents:
- 사업자등록증
- Service description (use the existing privacy-screen GPS section as the base; KCC accepts Korean-language plain-text descriptions)
- Designated 위치정보관리책임자 — typically the same person as the CPO from item 1
- Technical security measures description (200m radius, ±2h time window, no continuous tracking, server-side enforcement in migration
00131/00136— these are the talking points)
- Filing fee: free for 신고.
- Process: KCC reviews within 1-3 weeks; approval letter arrives by email.
- Once approved, update
PrivacyScreen"Location Information" section with the assigned 신고번호 + filing date.
Risk if skipped: Operating GPS features without the 신고 is a KRW 30M penalty under §43(3) + suspension order. Discovery vector is typically a user complaint → KCC audit. Korean media also covers these audits, so reputation cost can exceed the fine.
⛔ 4. iOS preview + production builds — gated on Apple Developer credentials
Why: Currently only Android builds exist on EAS (b3542ab9 production, e9483ebc preview). iOS App Store + TestFlight require:
- An active Apple Developer Program enrollment (USD $99/year)
- Either an Individual or Organization account (Organization is required for App Store Connect 'Team Members' separation, but Individual works for solo founders)
- A configured App Store Connect app record (Bundle ID =
com.ivorybridge.twomore, matching what's locked inapp.config.js)
Action:
- Enroll in Apple Developer Program if not already (https://developer.apple.com/programs/enroll/). Allow 1-2 days for D-U-N-S verification on Organization accounts.
- Register the Bundle ID
com.ivorybridge.twomorein Apple Developer → Certificates, Identifiers & Profiles → Identifiers → "+". - Create the app record in App Store Connect (https://appstoreconnect.apple.com → My Apps → "+" → New App). Bundle ID dropdown should now show
com.ivorybridge.twomore. - Run from repo root:bash
cd apps/mobile npx eas-cli credentials configure --platform ios # follow interactive prompts; EAS will mint a Distribution Certificate + Provisioning Profile - Trigger first preview build:bash
npx eas-cli build --profile preview --platform ios - Once preview validates on a real device, trigger production build:bash
npx eas-cli build --profile production --platform ios - Upload to TestFlight from EAS Dashboard or via
eas submit.
Risk if skipped: iOS launch is blocked. Android-only soft-launch is viable (Korea is ~30% iOS, ~70% Android, but the tennis demographic skews iOS-heavier — likely 45%+ of TAM unreachable without iOS).
⛔ 5. Branch protection on main
Why: CI is reliably green (every push runs typecheck + tests + lint:strict) but main is currently writable by anyone with push access. A bad merge with no CI gate could land directly. Branch protection makes CI a hard requirement, prevents force-push, and ensures the linear-history we rely on for the verify-no-stale-claims gate.
Action:
- GitHub Settings → Branches → https://github.com/ivorybridge-2025/twomore-v2/settings/branches
- Click "Add classic branch protection rule" (Classic, not "Rulesets" — simpler and adequate for solo repos).
- Branch name pattern:
main - Enable:
- [x] Require status checks to pass before merging
- Required check:
typecheck + tests + lints(the existing CI job name from.github/workflows/ci.yml) - Do NOT require
drift — architectural erosion(non-blocking by design — runs alongside but doesn't gate)
- Required check:
- [x] Require linear history (preserves
verify-no-stale-claimsclean commits) - [x] Do not allow bypassing the above settings (apply to admins too)
- [x] Require status checks to pass before merging
- Save.
Risk if skipped: A red-CI merge could land on main and break the next OTA. Low frequency, but the cost of one bad OTA hitting production users is high enough to justify the 2-minute setup.
➖ 6. SUPABASE_ACCESS_TOKEN repo secret (type-drift CI gate)
Why: .github/workflows/ci.yml has an opt-in job that compares the committed generated.types.ts against the live Supabase schema. Without the token, the job skips (silent green). With it, the job catches "schema changed in dashboard but types weren't regenerated" drift.
Action:
- Supabase Dashboard → Settings → Access Tokens → "Generate new token".
- GitHub repo Settings → Secrets and variables → Actions → New repository secret.
- Name:
SUPABASE_ACCESS_TOKEN. Paste token. - Next CI run automatically picks it up.
- Bonus: also regenerate
generated.types.tslocally once via:bashCommit only if the diff is intentional.npx supabase gen types typescript --linked \ | npx prettier --config .prettierrc.json --parser typescript \ > packages/app/src/adapters/supabase/generated.types.ts
Risk if skipped: Non-blocking. Schema drift catches will be late (next time someone manually runs the gen command), but no production impact.
⛔ 7. Production EAS Update channel + OTA pipeline — launch-gated
Why: Production users need JS-layer bugfixes without a full app-store resubmission.
Current state:
apps/mobile/eas.jsonsetsproduction.channel = "production".package.jsonprovides guardedyarn ota:production.package.jsonalso provides guardedyarn ota:production:canaryfor a 5% rollout.scripts/guard-ota-release.mjsblocks OTA from dirty, no-upstream, unpushed-ahead, or behind-upstream source states, and blocks production OTA unlessTWOMORE_ALLOW_PRODUCTION_OTA=1is set.
Current policy: before public launch, publish OTA updates only to preview:
yarn ota:previewAny significant app change must first be classified through Release Versioning And OTA Workflow: docs-only changes do not publish OTA, JS-only changes may publish preview OTA, and native/runtime changes require a preview build instead of OTA-only shipping. Artifact IDs, hosted smoke outcomes, failed attempts, and explicit no-artifact decisions are recorded in Release Evidence Ledger.
Remaining launch check: after R4 Launch Readiness is accepted, the first production OTA should be verified in EAS Dashboard → Updates and smoke-tested on an installed production build. Use canary first when possible:
TWOMORE_ALLOW_PRODUCTION_OTA=1 yarn ota:production:canary➖ 8. App Store / Play Store listing assets
Why: Each store requires icons, screenshots, descriptions, privacy questionnaires before approval.
Action checklist:
Apple App Store Connect:
- App icon (1024x1024 PNG) — use
apps/mobile/assets/icon.png(already generated, 1024x1024) - iPhone 6.7" screenshots (1290x2796) — capture 3-5 from live preview build
- iPad screenshots (if supporting iPad)
- App description (Korean — 4000 char limit), keywords, support URL, marketing URL
- App Privacy questionnaire — data types collected: identifiers (Supabase auth), contact info (email, phone), location (GPS), purchases (Portone — future), usage data (analytics), diagnostics (Sentry). Cross-border = yes (Supabase US, Sentry US, Expo Push US).
- Age rating: 12+ (per the under-14 PIPA gate; lower than 17+ since no objectionable content)
Google Play Console:
- App icon (512x512 PNG) — generate from
apps/mobile/assets/adaptive-icon.png - Feature graphic (1024x500)
- Phone screenshots (1080x1920 minimum)
- App description (Korean), short description
- Data safety section — same data types as App Privacy
- Content rating questionnaire
Risk if skipped: Store submission rejection or month-long review delays.
➖ 9. App-update gating (force-update path)
Why: When you ship a future native-module change that breaks API compatibility (e.g. a new runtime version, a Supabase client major bump), users on old builds can crash repeatedly until they upgrade. The current app has no force-update mechanism — a banner or hard-gate that prompts users to update when their runtimeVersion is below a server-defined minimum.
Action: Out of scope for first launch. Add as a CLAUDE.md "Next concrete steps" item for the 0.6.0 milestone.
✅ 10. Sentry sample-rate dial-back — tracesSampleRate / profilesSampleRate
Why: Full Sentry performance/profiling capture is useful for investigations but too expensive for public traffic.
Current state: packages/app/src/lib/sentry.ts uses launch defaults:
tracesSampleRate: 0.2;
profilesSampleRate: 0.1;Raise temporarily only when diagnosing a specific regression.
➖ 11. Dev-surface production gating — DevPanel + seed/simulate edge functions
Why: The in-app DevPanel (FAB + scenario seeder + telemetry/diagnostics) and the destructive seed edge functions must never be reachable by real users. They ARE gated today — this item is a verify-before-launch checkpoint, not new work.
Current gating (verify each still holds):
- DevPanel visibility —
packages/app/src/presentation/components/dev-panel/index.tsxreturnsnullunlessdevMode || isDevContext() || userId === DEV_USER_ID.isDevContext()is__DEV__ || NODE_ENV !== 'production'— both false in a production build. So the FAB only shows for the dev account or a dev/preview build. Confirm a release build with a non-dev account shows no FAB. - Telemetry RPCs —
dev_read_telemetry/dev_clear_telemetry(migration00198) raise42501for any caller whoseauth.uid()≠DEV_USER_ID, so even if the panel rendered, a non-dev user reads/clears nothing. Server-side enforced. - Destructive seed edge functions —
seed-scenario/simulate-activityreject non-local / non-allowlisted invocations (production-env guards + theSEED_ALLOWED_UIDSallowlist, defaulting to the dev account). Confirmseed-scenariois NOT callable by an arbitrary authenticated user in production.
Action: On the first production build, sign in with a NON-dev account and confirm: (a) no DevPanel FAB on the home tab, (b) supabase.rpc('dev_read_telemetry') returns a 42501 error, (c) seed-scenario invocation is rejected. Document the check in the rebuild-log.
Risk if skipped: If any gate regressed, a real user could wipe/seed production data or read the dev account's telemetry. All three gates exist today; this is a regression-catch checkpoint, hence ➖ (verify, don't build).
⛔ 12. HTTP Universal/App Links for share routes
Why: R1 sharing now uses canonical https://twomore.app URLs, and the app has custom-scheme recipient proof. Public launch still needs verified HTTP iOS Universal Links and Android App Links so recipients can tap a normal web URL from KakaoTalk, browser, or SMS and land in the installed app without relying on twomore://.
Current state:
apps/mobile/app.jsondeclaresios.associatedDomains = ["applinks:twomore.app"].apps/mobile/app.jsondeclares AndroidautoVerifyintent filters for/s,/m,/p,/lb, and/join-club.apps/web/app/.well-known/apple-app-site-association/route.tsserves the iOS association response whenTWOMORE_APPLE_TEAM_IDis set.apps/web/app/.well-known/assetlinks.json/route.tsserves the Android association response whenTWOMORE_ANDROID_SHA256_CERT_FINGERPRINTSis set.yarn check:r1-app-linksverifies the static repo contract and reports the missing external identifiers.yarn check:r1-app-links:remoteis the live-domain proof gate. As of 2026-06-28 it fails before credential comparison because both live association endpoints redirect from the canonical apex host towww:https://twomore.app/.well-known/apple-app-site-associationredirects tohttps://www.twomore.app/.well-known/apple-app-site-association, andhttps://twomore.app/.well-known/assetlinks.jsonredirects tohttps://www.twomore.app/.well-known/assetlinks.json. Thewwwtarget then redirects the.well-knownrequests to/auth/logininstead of serving association JSON.- Android preview build
6aa365f6-7814-4250-a708-a27843a919dcwas inspected withapksigner verify --print-certson 2026-06-28. Its public signing certificate SHA-256 fingerprint isF4:67:1F:16:B3:95:12:06:0C:20:4C:E4:5E:AA:79:F8:6A:C1:AD:C9:29:49:F4:D8:AE:EB:DB:6A:D3:D2:8B:3E. Treat this as the observed preview signing fingerprint, not production proof; recheck the final candidate build if signing credentials rotate.
Action:
- Obtain Apple Team ID from Apple Developer membership.
- Obtain or confirm SHA-256 certificate fingerprints for the EAS/Play signing certificates that should verify
com.ivorybridge.twomore. The current observed preview Android fingerprint is listed above; confirm it still matches the candidate build before final acceptance. - Set web deploy environment variables:
TWOMORE_APPLE_TEAM_IDandTWOMORE_ANDROID_SHA256_CERT_FINGERPRINTS. - Deploy the web app behind the canonical apex host
https://twomore.appwithout redirecting the two.well-knownassociation endpoints towww. - Run
yarn check:r1-app-links. - Run
yarn check:r1-app-links:remoteand require direct HTTP 200 JSON for both association endpoints. - Trigger a preview build because
apps/mobile/app.jsonnative config changed; do not treat OTA as sufficient. - Install the preview build on iOS and Android and verify these URLs open the installed app:
https://twomore.app/s/<sessionId>https://twomore.app/m/<matchId>https://twomore.app/p/<userId>https://twomore.app/lb/<clubId>https://twomore.app/join-club?code=<inviteCode>
- Record the build ID, device smoke, endpoint checks, and any rollback pointer in Release Evidence Ledger.
Risk if skipped: HTTP share links remain web-preview/custom-scheme only and R1-3 cannot be accepted as a full web-to-installed-app sharing loop.
⛔ 13. KakaoTalk Share templates for R1 growth links
Why: Korean club acquisition is expected to happen through KakaoTalk. R1 share cards now have a source-level Kakao template contract and fallback-safe native adapter wiring, but the app still needs credentialed native-build proof before Kakao-native sharing can be accepted. Public launch should not claim Kakao-native sharing until approved Kakao templates render in KakaoTalk and return recipients to useful app/web destinations.
Current state:
packages/app/src/config/kakao-share-templates.jsondefines the R1 custom-template manifest for session invite, club invite, match result, tier promotion, stat card, and leaderboard shares.packages/app/src/presentation/utils/share-cards.tsattacheskakaoTemplatemetadata to every R1 share-card composer.scripts/check-r1-kakao-share.mjsverifies static manifest coverage and reports missing external gates.yarn check:r1-kakao-sharepasses static coverage.yarn check:r1-kakao-share:credentialsintentionally fails until all sixTWOMORE_KAKAO_TEMPLATE_*IDs and eitherTWOMORE_KAKAO_NATIVE_APP_KEYor the existingEXPO_PUBLIC_KAKAO_APP_KEYfallback are available.@react-native-kakao/coreand@react-native-kakao/shareare installed in the mobile app shell and@twomore/app.apps/mobile/app.config.jsconditionally configures the Kakao core native plugin whenTWOMORE_KAKAO_NATIVE_APP_KEYexists, falling back toEXPO_PUBLIC_KAKAO_APP_KEYfor environments that already carry the Kakao Native App Key under the older login variable.packages/app/src/adapters/share/share.native.tstries Kakao custom-template sharing and falls back to the OS share sheet.- No preview build/device proof exists yet for KakaoTalk template rendering.
Action:
- In Kakao Developers, create and approve the six custom templates represented by the manifest keys:
session_inviteclub_invitematch_resulttier_promotionstat_cardleaderboard
- Set the template IDs in the appropriate preview environment:
TWOMORE_KAKAO_TEMPLATE_SESSION_INVITE,TWOMORE_KAKAO_TEMPLATE_CLUB_INVITE,TWOMORE_KAKAO_TEMPLATE_MATCH_RESULT,TWOMORE_KAKAO_TEMPLATE_TIER_PROMOTION,TWOMORE_KAKAO_TEMPLATE_STAT_CARD, andTWOMORE_KAKAO_TEMPLATE_LEADERBOARD. - Set
TWOMORE_KAKAO_NATIVE_APP_KEYin the preview build environment. If the target environment already exportsEXPO_PUBLIC_KAKAO_APP_KEYwith the same Kakao Native App Key, the native share plugin will use that value, but theTWOMORE_name remains preferred for R1 release evidence. - Create a preview build that includes the Kakao native plugin and share dependencies; do not ship this as OTA-only.
- Run:bash
yarn check:r1-kakao-share yarn check:r1-kakao-share:credentials yarn workspace @twomore/app test src/adapters/share/__tests__/share.native.test.ts --runInBand yarn workspace @twomore/mobile preflight yarn check - Smoke on installed iOS and Android preview builds with KakaoTalk installed: verify each R1 template renders expected title/body/buttons and returns to a useful
/s,/m,/p,/lb, or/join-club?code=<inviteCode>destination. - Record template IDs' environment target, preview build ID, device smoke results, fallback behavior, and rollback pointer in Release Evidence Ledger.
Risk if skipped: R1 sharing remains generic OS/web sharing. That is usable, but it does not prove the Kakao-native growth loop the roadmap depends on.
Quick scan
Open these files when you have the values:
| Item | File | Lines |
|---|---|---|
| CPO contact | packages/features/profile/src/privacy-screen.tsx | 254–257 |
| Policy version bump | packages/app/src/config/legal/policy-versions.ts | 20–43 |
| GPS regulatory mention | packages/features/profile/src/privacy-screen.tsx | — search 위치정보 |
| EAS iOS config | apps/mobile/app.config.js | — ios section |
| CI workflow | .github/workflows/ci.yml | — |
| Sentry sample rate | packages/app/src/lib/sentry.ts | — tracesSampleRate / profilesSampleRate |
| DevPanel prod gating | packages/app/src/presentation/components/dev-panel/index.tsx | — enabled guard |
| HTTP app links | apps/mobile/app.json | — associatedDomains / intentFilters |
| Kakao share templates | packages/app/src/config/kakao-share-templates.json | — template keys / env vars |
Out of scope for this checklist
These are tracked elsewhere and don't gate public launch:
- Production Kakao auth and real PortOne native adapter/console secrets → external-credential/runtime work tracked in CLAUDE.md
- Coaching marketplace → Phase 9
- Court booking SaaS → Phase 8
- Inter-club + seasons + chat → Phase 11
- Migration of legacy ANON_KEY → PUBLISHABLE_KEY in client.ts → Path B continuation (not blocking; both keys are valid)
- Web app launch (apps/web) → separate gate