Skip to content

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:

  1. 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).
  2. Provision a dedicated privacy@<domain> email mailbox + a real phone number (mobile is fine).
  3. Replace the placeholders in packages/features/profile/src/privacy-screen.tsx:254-257:
    '   · 성명: 이성규\n' +
    ...
    '   · 이메일: privacy@twomore.app\n' +
    '   · 전화: 02-0000-0000 (TODO: 실제 연락처로 교체 필요)\n\n' +
  4. Drop the (TODO: 실제 연락처로 교체 필요) annotation.
  5. Commit + push + yarn ota:preview while 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:

  1. Engage a Korean privacy / IT-law counsel (typical retainer: KRW 3-5M for a startup-tier MVP policy review, ~2-3 week turnaround).
  2. Provide them: current PrivacyScreen full 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).
  3. Apply edits to both *-screen.tsx files.
  4. Bump packages/app/src/config/legal/policy-versions.ts:
    ts
    terms:    { 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: ... },
  5. Ship a new migration 001XX_policy_versions_kr_1_0_1.sql that INSERTs a new row into public.policy_versions for each affected policy type, with the new content_hash. Existing users get re-prompted to consent to the new version next time they grant any consent.
  6. Update docs/rebuild-log.md with 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:

  1. Register a business entity (사업자등록) if not already done. Sole proprietor (개인사업자) suffices for the filing — corporate (법인) is not required.
  2. Go to https://lbsc.kr (KCC location-based-services registry).
  3. Choose 신고 (not 허가 — 허가 is for telco-level GPS providers).
  4. 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)
  5. Filing fee: free for 신고.
  6. Process: KCC reviews within 1-3 weeks; approval letter arrives by email.
  7. 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 in app.config.js)

Action:

  1. 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.
  2. Register the Bundle ID com.ivorybridge.twomore in Apple Developer → Certificates, Identifiers & Profiles → Identifiers → "+".
  3. 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.
  4. 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
  5. Trigger first preview build:
    bash
    npx eas-cli build --profile preview --platform ios
  6. Once preview validates on a real device, trigger production build:
    bash
    npx eas-cli build --profile production --platform ios
  7. 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:

  1. GitHub Settings → Branches → https://github.com/ivorybridge-2025/twomore-v2/settings/branches
  2. Click "Add classic branch protection rule" (Classic, not "Rulesets" — simpler and adequate for solo repos).
  3. Branch name pattern: main
  4. 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)
    • [x] Require linear history (preserves verify-no-stale-claims clean commits)
    • [x] Do not allow bypassing the above settings (apply to admins too)
  5. 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:

  1. Supabase Dashboard → Settings → Access Tokens → "Generate new token".
  2. GitHub repo Settings → Secrets and variables → Actions → New repository secret.
  3. Name: SUPABASE_ACCESS_TOKEN. Paste token.
  4. Next CI run automatically picks it up.
  5. Bonus: also regenerate generated.types.ts locally once via:
    bash
    npx supabase gen types typescript --linked \
      | npx prettier --config .prettierrc.json --parser typescript \
      > packages/app/src/adapters/supabase/generated.types.ts
    Commit only if the diff is intentional.

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:

  1. apps/mobile/eas.json sets production.channel = "production".
  2. package.json provides guarded yarn ota:production.
  3. package.json also provides guarded yarn ota:production:canary for a 5% rollout.
  4. scripts/guard-ota-release.mjs blocks OTA from dirty, no-upstream, unpushed-ahead, or behind-upstream source states, and blocks production OTA unless TWOMORE_ALLOW_PRODUCTION_OTA=1 is set.

Current policy: before public launch, publish OTA updates only to preview:

bash
yarn ota:preview

Any 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:

bash
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:

ts
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):

  1. DevPanel visibilitypackages/app/src/presentation/components/dev-panel/index.tsx returns null unless devMode || 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.
  2. Telemetry RPCsdev_read_telemetry / dev_clear_telemetry (migration 00198) raise 42501 for any caller whose auth.uid()DEV_USER_ID, so even if the panel rendered, a non-dev user reads/clears nothing. Server-side enforced.
  3. Destructive seed edge functionsseed-scenario / simulate-activity reject non-local / non-allowlisted invocations (production-env guards + the SEED_ALLOWED_UIDS allowlist, defaulting to the dev account). Confirm seed-scenario is 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).


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:

  1. apps/mobile/app.json declares ios.associatedDomains = ["applinks:twomore.app"].
  2. apps/mobile/app.json declares Android autoVerify intent filters for /s, /m, /p, /lb, and /join-club.
  3. apps/web/app/.well-known/apple-app-site-association/route.ts serves the iOS association response when TWOMORE_APPLE_TEAM_ID is set.
  4. apps/web/app/.well-known/assetlinks.json/route.ts serves the Android association response when TWOMORE_ANDROID_SHA256_CERT_FINGERPRINTS is set.
  5. yarn check:r1-app-links verifies the static repo contract and reports the missing external identifiers.
  6. yarn check:r1-app-links:remote is 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 to www: https://twomore.app/.well-known/apple-app-site-association redirects to https://www.twomore.app/.well-known/apple-app-site-association, and https://twomore.app/.well-known/assetlinks.json redirects to https://www.twomore.app/.well-known/assetlinks.json. The www target then redirects the .well-known requests to /auth/login instead of serving association JSON.
  7. Android preview build 6aa365f6-7814-4250-a708-a27843a919dc was inspected with apksigner verify --print-certs on 2026-06-28. Its public signing certificate SHA-256 fingerprint is F4: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:

  1. Obtain Apple Team ID from Apple Developer membership.
  2. 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.
  3. Set web deploy environment variables: TWOMORE_APPLE_TEAM_ID and TWOMORE_ANDROID_SHA256_CERT_FINGERPRINTS.
  4. Deploy the web app behind the canonical apex host https://twomore.app without redirecting the two .well-known association endpoints to www.
  5. Run yarn check:r1-app-links.
  6. Run yarn check:r1-app-links:remote and require direct HTTP 200 JSON for both association endpoints.
  7. Trigger a preview build because apps/mobile/app.json native config changed; do not treat OTA as sufficient.
  8. 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>
  9. 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.


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:

  1. packages/app/src/config/kakao-share-templates.json defines the R1 custom-template manifest for session invite, club invite, match result, tier promotion, stat card, and leaderboard shares.
  2. packages/app/src/presentation/utils/share-cards.ts attaches kakaoTemplate metadata to every R1 share-card composer.
  3. scripts/check-r1-kakao-share.mjs verifies static manifest coverage and reports missing external gates.
  4. yarn check:r1-kakao-share passes static coverage.
  5. yarn check:r1-kakao-share:credentials intentionally fails until all six TWOMORE_KAKAO_TEMPLATE_* IDs and either TWOMORE_KAKAO_NATIVE_APP_KEY or the existing EXPO_PUBLIC_KAKAO_APP_KEY fallback are available.
  6. @react-native-kakao/core and @react-native-kakao/share are installed in the mobile app shell and @twomore/app.
  7. apps/mobile/app.config.js conditionally configures the Kakao core native plugin when TWOMORE_KAKAO_NATIVE_APP_KEY exists, falling back to EXPO_PUBLIC_KAKAO_APP_KEY for environments that already carry the Kakao Native App Key under the older login variable.
  8. packages/app/src/adapters/share/share.native.ts tries Kakao custom-template sharing and falls back to the OS share sheet.
  9. No preview build/device proof exists yet for KakaoTalk template rendering.

Action:

  1. In Kakao Developers, create and approve the six custom templates represented by the manifest keys:
    • session_invite
    • club_invite
    • match_result
    • tier_promotion
    • stat_card
    • leaderboard
  2. 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, and TWOMORE_KAKAO_TEMPLATE_LEADERBOARD.
  3. Set TWOMORE_KAKAO_NATIVE_APP_KEY in the preview build environment. If the target environment already exports EXPO_PUBLIC_KAKAO_APP_KEY with the same Kakao Native App Key, the native share plugin will use that value, but the TWOMORE_ name remains preferred for R1 release evidence.
  4. Create a preview build that includes the Kakao native plugin and share dependencies; do not ship this as OTA-only.
  5. 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
  6. 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.
  7. 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:

ItemFileLines
CPO contactpackages/features/profile/src/privacy-screen.tsx254–257
Policy version bumppackages/app/src/config/legal/policy-versions.ts20–43
GPS regulatory mentionpackages/features/profile/src/privacy-screen.tsx— search 위치정보
EAS iOS configapps/mobile/app.config.js— ios section
CI workflow.github/workflows/ci.yml
Sentry sample ratepackages/app/src/lib/sentry.tstracesSampleRate / profilesSampleRate
DevPanel prod gatingpackages/app/src/presentation/components/dev-panel/index.tsxenabled guard
HTTP app linksapps/mobile/app.jsonassociatedDomains / intentFilters
Kakao share templatespackages/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

Markdown remains the source of truth. Run yarn docs:check before handoff.