Skip to content

R1 Activation Source Pack

Status: Active Last reviewed: 2026-06-29 Slice: R1-0 Activation source pack, R1-1 Intent-aware onboarding routing, accepted R1-2 Club discovery and creation first-session loop, and active R1-3 Share and deep-link activation

This is the source-grounded inventory for the R1 Club Growth Loop. It records what the app currently does, where future agents should edit, and which proof is required before R1 can move from "planned loop" to accepted product evidence.

Use this with:

R1-0 Acceptance

R1-0 is accepted because the current onboarding, club discovery, invite, share, deep-link, and club_growth surfaces have been traced to source files, current gaps are listed, and test/smoke entry points are named below.

R1-0 does not mean the club-growth loop is fully accepted. It only means R1 now has a reliable source pack for implementation.

R1-1 Acceptance

R1-1 is accepted because onboarding completion no longer blindly routes every new user to Home. The mobile root AuthGate resolves the current route and global search params through resolveOnboardingDestination, passes the resolved destination into OnboardingWizardScreen, and the wizard preserves its existing birth-year, consent, profile, and completion write contract before replacing to that destination.

Accepted intent map:

SignalIntentPost-onboarding destination
No route or intent signalnew_playerroutes.home
intent=club_member, /clubs, or /clubs/discoverclub_memberroutes.discoverClubs
intent=club_admin or /create-clubclub_adminroutes.createClub
intent=pickup_host or /create-session?clubId=pickuppickup_hostroutes.createSession('pickup')
intent=invitee or /join-clubinviteeroutes.joinClub
Public share receiver /s, /m, /p, or /lbinviteeOriginal public receiver path
Safe internal next=/...return_to_link or inferred action intentOriginal next destination

Unsafe or looping next values are rejected before onboarding completion: absolute URLs, protocol-relative URLs, backslash paths, /login, and /profile-setup fall back to the default new-player route.

Verification:

  • yarn workspace @twomore/app test src/navigation/onboarding-intent.test.ts --runInBand
  • yarn check

The active slice is R1-3 Share and deep-link activation.

R1-2 Accepted Implementation State

R1-2 is accepted. The first implementation pass found and fixed a server-boundary drift risk: club_members_insert allowed any authenticated user to self-insert as member for any known club_id, while product copy and public profiles differentiated open, approval, and invite_only clubs.

Current codebase state:

  • public.join_open_club(club_id, idempotency_key) and public.join_club_by_invite_code(code, idempotency_key) are added in migration 00304_club_join_rpc_hardening.sql.
  • 00305_signal_immutability_invoker_boundary.sql fixes the follow-up signal boundary bug found by direct RPC probing: trusted database signal writers now bypass signal immutability by SQL execution role, while direct app writes stay lifecycle-only.
  • Direct self-insert into club_members is removed from RLS; admins can still add members, and join flows use RPCs.
  • useJoinClub delegates invite-code joins to the invite RPC.
  • useJoinOpenClub delegates public-profile open joins to the open-join RPC.
  • Public club profile now routes open clubs through direct join and then club detail; invite_only clubs still route to /join-club.
  • Open-club public profile navigation now observes the open-join mutation success state directly, so a successful join_open_club result should land on the member club-detail route even if per-call mutation callbacks are missed.
  • Approval-club public profiles move the open application-panel submit action into the pinned BottomCtaBand as club.public-profile.application-submit with visible text 신청 보내기, so the screen has one canonical primary CTA while the panel is open.
  • Created-club owner detail now exposes club.detail.create-session-shortcut beside the empty "다가오는 일정" section, so the first-session action appears where the empty schedule is shown instead of relying only on the screen-level bottom CTA.
  • club_growth now contains a repeatable JOINR12 invite-code fixture and prunes fixture memberships back to the expected stable member set before every seed run.
  • 09-club-join-funnels.yaml covers invite-code join, open-club direct join, and approval-request submission.
  • 10-club-create-first-session-cta.yaml covers the zero-club create-club wizard, created-owner club detail, empty-schedule first-session shortcut, and create-session wizard entry.

Preview Supabase state:

  • yarn supabase db push --linked applied 00304_club_join_rpc_hardening.sql and 00305_signal_immutability_invoker_boundary.sql to the linked preview project.
  • yarn supabase migration list --linked shows local and remote both at 00305.
  • yarn supabase functions deploy seed-scenario deployed the updated club_growth fixture-prune and stable member-selection logic to the linked preview project.
  • node scripts/run-scenario.mjs club_growth passed with users=29, clubs=9, sessions=0, memberships=134, media=10, joinRequests=7, prunedMemberships=0, and fixturePrunedMemberships=0 in steady state.
  • Direct authenticated RPC probe passed for join_club_by_invite_code('JOINR12'), returning one active member row.
  • Direct authenticated RPC probe passed for open join on 성동 베어본 테니스, returning one active member row.
  • Preview fixture count after reset: JOINR12 club is back to 8/20 active members, and the open-join probe membership is pruned back out of the discoverable fixture.

Preview mobile update state:

  • Initial R1-2 join hardening OTA published source state 1f3342d802cf096fecb927df25f307491394c11a.
  • Target: preview branch/channel, runtime 0.6.1, platforms android and ios.
  • Initial EAS update group: 8f4499e4-6be2-4671-be23-69214ca073ce.
  • Android update ID: 019f0a9e-75fd-7210-9620-826817944e93.
  • iOS update ID: 019f0a9e-75fd-72ce-aed0-d25865051824.
  • Follow-up open-join navigation and seed-stabilization OTA published committed source state 93de2c86e5ed1de45bced234ee5bd24fe6c89760.
  • Follow-up EAS update group: 1ac260bd-fe8e-49e3-924d-b7ce4619217a.
  • Android update ID: 019f0b09-7081-7b87-8bfd-5bcfd00ce38a.
  • iOS update ID: 019f0b09-7081-7b8f-b27e-5cebd9a94c62.
  • Approval application submit stabilization OTA published committed source state 3645791c46dc3d61a0e0cd4ba7074d4169358d94.
  • Approval stabilization EAS update group: 28950118-aeca-42a3-8de1-ec3e9df218e6.
  • Android update ID: 019f0b2a-3bea-7206-975e-23e714b8898b.
  • iOS update ID: 019f0b2a-3bea-7435-9c8a-652a36909005.
  • Flow-only smoke alignment commit 7c0bd42756e424f55518d58a874a0c34093a9d7d is pushed to origin/main; no additional OTA was required because it changed Maestro YAML only.
  • First-session shortcut stabilization OTA published committed source state a5d2a9135ed8a5f5dd3cf11ab6cc8cdbe95f3b34.
  • First-session shortcut EAS update group: ff478b2e-4058-49ce-8d0d-333b66eabaa9.
  • Android update ID: 019f0bb0-b9bc-7c87-8581-d6aa951247a3.
  • iOS update ID: 019f0bb0-b9bc-755f-beee-8a96b9f4e137.

Hosted/mobile evidence:

  • Hosted rerun 019f0b37-4a40-7eff-8727-d31d22a43a43 passed enabled flows 01-login, 02-main-tabs-smoke, 08-club-growth-media, and 09-club-join-funnels using Android preview build 6aa365f6-7814-4250-a708-a27843a919dc.
  • Targeted hosted workflow 019f0bb1-2f5f-7845-bc8b-3d8ab65bbdfe reused the same Android preview build after preview OTA ff478b2e, seeded r1_create_club_zero, and passed 10-club-create-first-session-cta.yaml. Artifacts: JUnit report 019f0bb5-212d-7c29-ae01-2c7b796d8360; Maestro results 019f0bb5-1f40-730a-9b31-5c27170d5fd5.

R1-2 is accepted because backend join policy enforcement, invite/open/approval join proof, zero-club creation, and first-session action proof are all tied to committed source, preview OTA, deterministic seed, and hosted device evidence.

R1-3 Active Implementation State

R1-3 is active, not accepted. The first source slice fixed a drift-prone share URL contract:

  • packages/app/src/navigation/routes.ts now owns public share route helpers: publicSessionShare, publicMatchShare, publicProfileShare, publicLeaderboardShare, and publicClubInviteShare.
  • buildSessionShareCard, buildMatchShareCard, buildStatCardShareCard, buildLeaderboardShareCard, and buildClubInviteShareCard now generate URLs through those route helpers instead of hand-written path strings.
  • Club invite shares now link to /join-club?code=<inviteCode> while still carrying the invite code in the message body.
  • apps/mobile/app/join-club.tsx and apps/web/app/join-club/page.tsx read the code query and pass it into the shared JoinClubScreen.
  • JoinClubScreen normalizes linked and typed invite codes through normalizeInviteCodeInput, so linked invites prefill the same field used by manual invite-code entry.
  • Focused tests cover route helper encoding, share-card URLs, and invite-code normalization.

The second source slice fixed public receiver proof instead of relying on incidental readable table data:

  • 00306_public_share_previews.sql adds public.get_public_share_preview(kind, id), a SECURITY DEFINER RPC that exposes only the small public preview field set for sessions, matches, profiles, and discoverable club leaderboards.
  • Public web receiver pages and opengraph-image handlers for /s, /m, /p, and /lb now read through apps/web/lib/public-share-preview.ts instead of unauthenticated table queries.
  • OpenInAppButton is server-rendered and exposes a real twomore://... anchor in HTML, so the web fallback is visible without hydration while true universal/app links remain a separate native-build gate.
  • club_growth now returns deterministic share fixture IDs: shareClubId, shareSessionId, shareMatchId, shareProfileId, and shareInviteCode.
  • yarn smoke:r1-share-receivers --seed --base-url=http://127.0.0.1:3100 signs in only to seed/discover fixture IDs, then fetches receiver pages and OG images unauthenticated.

The third source slice prepared installed-preview custom-scheme proof without hardcoding fixture IDs:

  • scripts/prepare-r1-share-deep-link-flow.mjs signs in with dev credentials, invokes deployed club_growth, reads shareProfileId, shareSessionId, shareClubId, shareMatchId, and shareInviteCode from the seed summary, and writes ignored generated YAML at apps/mobile/.maestro/generated/r1-share-deep-links.yaml.
  • yarn prepare:r1-share-deep-link-flow prepares the generated flow; yarn e2e:r1-share-deep-links prepares it and runs Maestro locally when the CLI and a preview build are available.
  • apps/mobile/.eas/workflows/maestro-r1-share-deep-links-rerun.yml reuses an existing Android preview build, seeds/generates the flow in before_maestro_tests, and runs only the generated R1-3 custom-scheme flow.
  • scripts/prepare-r1-share-deep-link-flow.mjs now asserts the first-viewport public-club 가입하기 CTA after the /lb receiver, avoiding the stale below-the-fold 소식 tab assertion that can fail after a correct route land.
  • scripts/prepare-r1-share-deep-link-flow.mjs also opens twomore://join-club?code=<inviteCode>, verifies the join screen prefilled the seeded invite code, and submits without typing so the generated flow can prove invite-link return once hosted rerun evidence exists.

The fourth source slice made Kakao-native share work mechanically trackable without claiming native delivery:

  • packages/app/src/config/kakao-share-templates.json defines the R1 Kakao custom-template manifest for session_invite, club_invite, match_result, tier_promotion, stat_card, and leaderboard.
  • ShareContent now carries optional kakaoTemplate metadata, and every R1 share-card composer attaches validated template-key/argument payloads beside the existing OS/web share title, message, and URL.
  • scripts/check-r1-kakao-share.mjs verifies manifest coverage, share-card references, package script wiring, iOS Kakao schemes, and native-module state. The normal yarn check:r1-kakao-share mode passes static source coverage and reports missing external gates; credential mode yarn check:r1-kakao-share:credentials intentionally fails until TWOMORE_KAKAO_TEMPLATE_* IDs and a Kakao native app key are available.
  • Source commit 422037f28e815f17c179f54d2a033ac40bda3c60 is a JS/source contract slice only. It does not replace the current OS/web share adapter and does not prove KakaoTalk template rendering.

The fifth source slice wired the native Kakao Share adapter without claiming credentialed delivery:

  • @react-native-kakao/core and @react-native-kakao/share are installed in the mobile app shell and exposed to @twomore/app through the existing peer/development dependency pattern.
  • apps/mobile/app.config.js maps configured TWOMORE_KAKAO_TEMPLATE_* IDs into Expo extra and conditionally applies the @react-native-kakao/core config plugin when TWOMORE_KAKAO_NATIVE_APP_KEY is present, falling back to EXPO_PUBLIC_KAKAO_APP_KEY when that existing login variable carries the same Kakao Native App Key.
  • packages/app/src/adapters/share/share.native.ts now attempts shareCustomTemplate when a share card has kakaoTemplate metadata and a valid template ID. Missing IDs, missing native linkage, and native SDK errors fall back to React Native Share.share.
  • packages/app/src/adapters/share/__tests__/share.native.test.ts covers Kakao success, missing-template fallback, native failure fallback, and generic non-Kakao share content.
  • scripts/check-r1-kakao-share.mjs now checks the native app-key gate and the installed native modules. Normal mode passes static coverage; credential mode intentionally fails until the six template IDs and either TWOMORE_KAKAO_NATIVE_APP_KEY or EXPO_PUBLIC_KAKAO_APP_KEY exist.
  • Source commit fb2fd1bd76835a6921f1623b8c9600da2b3f6796 is native adapter wiring only. Because it adds native dependencies/config, acceptance requires a preview build plus KakaoTalk device smoke before anyone can claim Kakao-native delivery.

Preview mobile update state:

  • R1-3 share-link contract OTA published source state bf3c8746c18401e95378eca40ab3345a180ff890.
  • R1-3 EAS update group: 3ea7b802-e498-4197-a081-0284f3f49c12.
  • Android update ID: 019f0bd5-d1c4-71eb-96b3-813f3e227785.
  • iOS update ID: 019f0bd5-d1c4-7612-938e-00fe4198e90c.
  • Runtime: 0.6.1.
  • R1-3 mobile receiver fallback OTA published source state 111dbf77eb590b2ee6a49b0c70bffc3c2a5a342b.
  • R1-3 mobile receiver fallback EAS update group: e01fc19c-19a8-4012-8ec8-1555edad14d6.
  • Android update ID: 019f0c2b-2fae-7354-9ef0-fcd290f57b9a.
  • iOS update ID: 019f0c2b-2fae-763f-8729-dfe8098aa2df.
  • Runtime: 0.6.1.
  • Hosted generated custom-scheme proof: 019f0c32-1afc-70c5-82d8-185df88e8ad4 passed against Android preview build 6aa365f6-7814-4250-a708-a27843a919dc. The pass covered twomore://p, twomore://s, twomore://lb, and twomore://m with seeded club_growth share IDs.
  • Hosted generated invite-link proof: 019f0c63-85bc-7f19-9d9b-d449ed548eb1 passed against the same Android preview build. The pass covered twomore://join-club?code=JOINR12, asserted the join screen and prefilled code, submitted without typing, and landed on 중랑 초대코드 조인 테스트.

R1-3 remains active because these source slices prove the JS/web contract, seeded web receiver behavior, hosted Android custom-scheme return path, and static Kakao template metadata/native adapter contract, but they do not prove Kakao-native delivery or true HTTP universal/app links. Kakao-native work still needs Kakao Developers app/template credentials, a preview build with the Kakao native app key and the six template IDs, and KakaoTalk device smoke. Universal/app-link static config is now wired for twomore.app, but credentialed association output, live-domain deployment, preview-build install, and iOS/Android device smoke are still required; they must not be claimed from an OTA-only change. Source commit e6f60c2850dbff3dfc6a58a6642e0f1e6c3b240c adds yarn check:r1-app-links:remote for the live-domain proof. The first remote run fails at the Apple association URL because the canonical apex host redirects to www instead of serving this app's .well-known JSON. Follow-up source commit ee49c9d75f59cc6033596d75c45f76b22b85d13f repairs the remote guard's Android fingerprint parser, so the same command can validate comma-separated Android SHA-256 fingerprints once the live Apple/domain blocker is cleared. Source commit 64ea165c7651226f4f9a316b7de581d1cc353a92 adds yarn check:r1-release-readiness:eas as the strict aggregate R1 gate and yarn report:r1-release-readiness as the non-failing handoff report. Current report mode confirms the static contracts pass, but R1 is still blocked by missing app-link/Kakao credentials, the live .well-known redirect, missing credentialed EAS preview env names, an Android preview build behind native-sensitive source, and absent iOS preview build evidence. The current Android preview APK 6aa365f6-7814-4250-a708-a27843a919dc has observed signing SHA-256 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, but that fingerprint is not yet configured in the live web deploy env and must be rechecked on the final candidate build if credentials rotate.

Production-readiness deferral, 2026-06-29: the owner has deferred remaining external credentials, live-domain association deployment, compatible preview build/baseline advancement, iOS preview build, and device smoke proof to Production Readiness / R4. This does not mean the Kakao-native or HTTP universal/app-link gates are accepted. It means R1 source agents should stop reopening these blockers after they are proven owner/vendor/deployment-controlled, keep the mechanical report visible, and route the work through External Blockers and Pre-launch Checklist.

Current Product Path

StepCurrent behaviorSource surfacesStatus
Onboarding completionThe onboarding wizard collects birth year, consent, display name, and play style, then routes through the root-gate-selected onboarding destination. The default remains routes.home; explicit intents, action routes, public share receivers, and safe internal next links can route to the first useful action.apps/mobile/app/_layout.tsx; packages/app/src/navigation/onboarding-intent.ts; packages/app/src/navigation/onboarding-intent.test.ts; packages/features/onboarding/src/onboarding-wizard-screen.tsx; packages/features/onboarding/src/onboarding-step4-done.tsx; packages/app/src/navigation/routes.tsBuilt, intent-aware on mobile
Club discoveryThe Clubs tab has My Clubs and Discover tabs. Discovery cards open a public club preview. Zero-club empty state points to discovery.packages/features/clubs/src/club-list-screen.tsx; packages/features/clubs/src/clubs-discover-screen.tsx; packages/features/clubs/src/shared/discover-club-card.tsxBuilt
Create clubThe Clubs tab bottom CTA opens the create-club wizard. After creation, the owner lands on club detail and sees a first-session shortcut in the empty upcoming-schedule section plus the screen-level bottom CTA.packages/features/clubs/src/create-club-screen.tsx; packages/features/clubs/src/club-detail/home-tab.tsx; apps/mobile/app/create-club.tsx; apps/web/app/create-club/page.tsx; apps/mobile/.maestro/flows/10-club-create-first-session-cta.yamlBuilt; hosted 10 green
Join open clubPublic club profile joins open clubs directly through join_open_club, then replaces to club detail. The database, not the client, enforces open policy, discoverability, archive state, duplicate membership, and capacity.packages/features/clubs/src/public-club-profile-screen.tsx; packages/app/src/presentation/hooks/mutations/use-join-club.ts; packages/app/src/adapters/supabase/club.supabase.ts; supabase/migrations/00304_club_join_rpc_hardening.sqlDirect RPC green; hosted 09 green
Join by invite code/join-club lets a user enter an invite code, joins through join_club_by_invite_code, and redirects to club detail. Invite-code membership is now server-enforced instead of client raw-insert. Shared invite links can also prefill the field through /join-club?code=<inviteCode>.packages/features/clubs/src/join-club-screen.tsx; packages/features/clubs/src/join-club-utils.ts; packages/app/src/presentation/hooks/mutations/use-join-club.ts; packages/app/src/adapters/supabase/club.supabase.ts; apps/mobile/app/join-club.tsx; apps/web/app/join-club/page.tsx; supabase/migrations/00304_club_join_rpc_hardening.sql; supabase/migrations/00305_signal_immutability_invoker_boundary.sqlDirect RPC green; hosted 09 green; prefill tests green
Apply to approval clubPublic club profile records funnel events, opens an inline application panel for approval clubs, and submits a join request through the pinned submit CTA while the panel is open.packages/features/clubs/src/public-club-profile-screen.tsx; packages/app/src/presentation/hooks/mutations/use-apply-to-club.ts; packages/app/src/presentation/hooks/mutations/use-record-club-growth-event.tsBuilt; hosted 09 green for seeded request submission
Session interestPublic club profile can open the next public session via the session detail route.packages/features/clubs/src/public-club-profile-screen.tsx; packages/app/src/navigation/routes.tsBuilt
Session RSVPSession detail owns RSVP behavior; R1 smoke still needs deterministic RSVP proof in the club-growth activation path.packages/features/sessions/src/session-detail-screen.tsx; packages/app/src/presentation/hooks/mutations/use-rsvp.tsBuilt, R1 proof incomplete
ShareShare hooks build configuration-driven share cards, then use the platform share service. Native tries Kakao custom-template sharing when template metadata and IDs are available, then falls back to React Native Share.share; web uses navigator.share with clipboard fallback. Public share URLs are generated through canonical routes.public*Share helpers. R1 share cards carry Kakao custom-template metadata.apps/mobile/app.config.js; apps/mobile/package.json; packages/app/package.json; packages/app/src/domain/entities/share.entity.ts; packages/app/src/config/kakao-share-templates.json; packages/app/src/ports/services/share.service.port.ts; packages/app/src/adapters/share/share.native.ts; packages/app/src/adapters/share/__tests__/share.native.test.ts; packages/app/src/adapters/share/share.web.ts; packages/app/src/navigation/routes.ts; packages/app/src/presentation/utils/share-cards.ts; packages/app/src/presentation/hooks/use-share-session.ts; packages/app/src/presentation/hooks/use-share-club-invite.ts; scripts/check-r1-kakao-share.mjsBuilt with OS/web fallback and Kakao-native adapter wiring; template IDs, Kakao native app key (TWOMORE_KAKAO_NATIVE_APP_KEY preferred, EXPO_PUBLIC_KAKAO_APP_KEY fallback), preview build, and KakaoTalk device smoke pending; route/card tests, adapter tests, and yarn check:r1-kakao-share green
Public share landingWeb has landing pages and OG images for session, match, profile, and leaderboard links. Public preview reads go through bounded RPC get_public_share_preview instead of broad table RLS. Mobile has matching receiver routes; /s and /m use public preview fallback when private member reads are blocked. Club invite shares now use /join-club?code=<inviteCode> rather than the base web URL.apps/web/app/s/[sessionId]/page.tsx; apps/web/app/m/[matchId]/page.tsx; apps/web/app/p/[userId]/page.tsx; apps/web/app/lb/[clubId]/page.tsx; apps/web/app/*/opengraph-image.tsx; apps/web/lib/public-share-preview.ts; apps/web/lib/app-link-association.ts; apps/web/app/.well-known/*/route.ts; supabase/migrations/00306_public_share_previews.sql; apps/mobile/app/s/[sessionId].tsx; apps/mobile/app/m/[matchId].tsx; apps/mobile/app/p/[userId].tsx; apps/mobile/app/lb/[clubId].tsx; apps/mobile/app/join-club.tsx; apps/web/app/join-club/page.tsx; scripts/prepare-r1-share-deep-link-flow.mjs; scripts/check-r1-app-links.mjsBuilt with custom-scheme open-in-app and static HTTP app-link config; seeded web receiver smoke green; hosted Android generated custom-scheme smoke green including invite-code join; remote app-link proof command exists but currently fails because twomore.app redirects .well-known to www
Hosted club-growth smokeHosted EAS workflow builds or reuses preview Android, seeds club_growth, and runs login, tabs, club-growth media/profile/admin smoke, and R1-2 join funnels. The targeted R1 create workflow reuses preview Android, seeds r1_create_club_zero, and runs the zero-club create proof. The targeted R1-3 share workflow reuses preview Android, seeds club_growth, generates share IDs, and opens all custom-scheme share receivers. The consolidated R1 evidence workflow runs those three proof groups sequentially against one existing preview build.apps/mobile/.eas/workflows/maestro-e2e.yml; apps/mobile/.eas/workflows/maestro-e2e-rerun.yml; apps/mobile/.eas/workflows/maestro-r1-create-club-rerun.yml; apps/mobile/.eas/workflows/maestro-r1-share-deep-links-rerun.yml; apps/mobile/.eas/workflows/maestro-r1-evidence-rerun.yml; apps/mobile/.maestro/flows/08-club-growth-media.yaml; apps/mobile/.maestro/flows/09-club-join-funnels.yaml; apps/mobile/.maestro/flows/10-club-create-first-session-cta.yaml; docs/maestro-e2e.md; scripts/run-scenario.mjs; scripts/check-r1-evidence-workflow.mjs01/02/08/09, targeted 10, and targeted R1-3 custom-scheme flow pass on hosted reruns. The consolidated packet workflow is wired and statically guarded but still needs a green packet run before R1-4 can be accepted.

Route Inventory

Route familyMobile routesWeb routesPurpose
Homeapps/mobile/app/(tabs)/(home)/index.tsxapps/web/app/(tabs)/home/page.tsxDefault post-onboarding destination and daily attention router
Clubsapps/mobile/app/(tabs)/(clubs)/index.tsx; apps/mobile/app/(tabs)/(clubs)/discover.tsx; apps/mobile/app/clubs/[clubId]/*apps/web/app/(tabs)/clubs/page.tsx; apps/web/app/clubs/[clubId]/*Club list, discovery, public preview, member/admin detail
Join/createapps/mobile/app/join-club.tsx; apps/mobile/app/create-club.tsxapps/web/app/join-club/page.tsx; apps/web/app/create-club/page.tsxInvite-code join and club creation
Sessionsapps/mobile/app/clubs/[clubId]/sessions/*; apps/mobile/app/sessions/[sessionId]/*apps/web/app/clubs/[clubId]/sessions/page.tsx; session routes through shared screens where availableSession detail, RSVP, attendance, host/admin work
Public shareapps/mobile/app/s/[sessionId].tsx; apps/mobile/app/m/[matchId].tsx; apps/mobile/app/p/[userId].tsx; apps/mobile/app/lb/[clubId].tsxapps/web/app/s/[sessionId]/page.tsx; apps/web/app/m/[matchId]/page.tsx; apps/web/app/p/[userId]/page.tsx; apps/web/app/lb/[clubId]/page.tsxLink recipients can preview on web or open the installed app
Leaderboard/profileapps/mobile/app/leaderboard.tsx; apps/mobile/app/profile/[userId].tsx; apps/mobile/app/records/leaderboard.tsxapps/web/app/leaderboard/page.tsx; apps/web/app/profile/[userId]/page.tsx; apps/web/app/records/leaderboard/page.tsxShared profile and records destinations

The canonical route helper lives in packages/app/src/navigation/routes.ts. Add route-helper tests in packages/app/src/navigation/routes.test.ts when R1 changes route shape.

Evidence Catalog

Rule IDSource-backed ruleApplicability to TwoMoreCurrent state
R1-E1HTTP app links require native app association and website-hosted association files. Use iOS Associated Domains and Android App Links, not just a custom scheme, for true web-to-app routing. Sources: Expo iOS universal links, Expo Android app links.R1 share links at https://twomore.app/s, /m, /p, /lb, and /join-club.Static/native config is now wired: share cards default to https://twomore.app, apps/mobile/app.json declares ios.associatedDomains = ["applinks:twomore.app"], Android intentFilters cover the R1 share paths with autoVerify, and the web app serves credential-gated .well-known handlers. yarn check:r1-app-links:remote now verifies live endpoints without redirects. Final platform proof is still gated by the live twomore.app deployment, TWOMORE_APPLE_TEAM_ID, and TWOMORE_ANDROID_SHA256_CERT_FINGERPRINTS.
R1-E2Custom schemes can open an installed app but are not the same as verified universal/app links.Current web landing pages use twomore://... from OpenInAppButton.Built as a server-rendered fallback/current behavior; universal/app link entitlements remain R1-3 work.
R1-E3Rich social previews in the Next App Router should use route metadata and/or opengraph-image route handlers. Source: Next.js metadata and OG image docs.Session, match, profile, and leaderboard share links.Built for /s, /m, /p, and /lb with server metadata plus OG images, now backed by bounded public preview RPC get_public_share_preview.
R1-E4KakaoTalk share requires Kakao Developers app setup and Kakao share/template integration. Source: Kakao Developers KakaoTalk Share docs.Korean acquisition loop and club/session invitations.Static template metadata now exists for the R1 share kinds in kakao-share-templates.json, every R1 share composer carries kakaoTemplate, and yarn check:r1-kakao-share verifies source coverage. Credential mode still reports missing TWOMORE_KAKAO_TEMPLATE_* IDs and a Kakao native app key unless TWOMORE_KAKAO_NATIVE_APP_KEY or EXPO_PUBLIC_KAKAO_APP_KEY is exported, so KakaoTalk rendering/return is not accepted.
R1-E5EAS Update targeting is controlled by update channels and compatible runtime versions. Sources: EAS Update deployment, EAS runtime versions.Deciding whether an R1 change can ship by preview OTA or requires a preview build.runtimeVersion.policy = "appVersion" and current policy is preview-only OTA until R4. R1 client changes have shipped to preview update groups 8f4499e4-6be2-4671-be23-69214ca073ce, 1ac260bd-fe8e-49e3-924d-b7ce4619217a, 28950118-aeca-42a3-8de1-ec3e9df218e6, ff478b2e-4058-49ce-8d0d-333b66eabaa9, and 3ea7b802-e498-4197-a081-0284f3f49c12 on runtime 0.6.1.
R1-E6Expo Router provides the current normalized pathname without search params and global search params that include dynamic route params. Source: Expo Router URL parameters.Preserving or deriving onboarding return destinations in the mobile root auth gate.R1-1 uses usePathname() + useGlobalSearchParams() only in the mobile platform shell, then delegates to a pure resolver in @twomore/app.
R1-E7Expo Router's current protected-route guidance treats route availability as a router-level concern. Source: Expo Router protected routes.Future auth-shell redesigns could move toward declarative protected routes.R1-1 stays with the existing root AuthGate to avoid a broad auth-shell rewrite while still centralizing intent routing.
R1-E8Join policy must be enforced at the database boundary, not only in public-profile UI. The former club_members_insert self-join policy could not see an invite code or distinguish open/approval/invite-only intent.R1-2 zero-club club joins and invite-code joins.Migration 00304_club_join_rpc_hardening.sql moves open and invite joins to RPCs and leaves direct member insert to club admins. Migration 00305_signal_immutability_invoker_boundary.sql keeps signal side effects from blocking authenticated RPC-backed joins.

Known Gaps

GapWhy it mattersNext slice
Kakao-native delivery is not provenKorean growth is likely KakaoTalk-led. The Kakao template contract and native adapter now exist, but approved template IDs, a preview build with the Kakao native app key, and KakaoTalk template/device proof do not. These external credentials/build/smoke gates are deferred to Production Readiness, not accepted.R4 Production Readiness
True universal/app links still lack credentialed proofStatic/native config and .well-known handlers are wired for twomore.app, but iOS still needs Apple Team ID in TWOMORE_APPLE_TEAM_ID, Android needs the observed preview signing SHA-256 configured in TWOMORE_ANDROID_SHA256_CERT_FINGERPRINTS and rechecked on the candidate build, and both platforms need a preview build/device smoke. These external domain/build/smoke gates are deferred to Production Readiness, not accepted.R4 Production Readiness
Hosted club-growth smoke excludes RSVP, share-session, and onboarding account proofThe EAS workflows explicitly exclude 03-rsvp-toggle.yaml, 06-share-session.yaml, and 07-onboarding-under-14-gate.yaml until deterministic seed/account prerequisites exist. The R1 packet proves the currently seeded activation path; it must not be described as RSVP acceptance yet.R1-4
Scenario catalog has two layersscripts/run-scenario.mjs club_growth runs the deployed club_growth scenario key. The dev panel builder preset also composes club_growth with format/nature/upcoming overlays through packages/app/src/presentation/components/dev-panel/types.ts.R1-4, if terminal/dev-panel parity is required
Production Kakao auth/share is not provenProduct docs mark Kakao auth/share as scaffolded or built, but production credentials and Kakao-native share remain external/platform work.R4 Production Readiness

Code Owners By Surface

These are code ownership surfaces, not people.

SurfacePrimary pathsNotes
Onboardingapps/mobile/app/_layout.tsx; packages/app/src/navigation/onboarding-intent.ts; packages/features/onboarding/src/*; packages/app/src/presentation/screens/auth/*; packages/app/src/config/i18n/*/onboarding.tsKeep consent/under-14 gates intact; intent routing belongs in the root gate and pure resolver, not inside consent/profile writes.
Club discovery and joinpackages/features/clubs/src/club-list-screen.tsx; packages/features/clubs/src/clubs-discover-screen.tsx; packages/features/clubs/src/public-club-profile-screen.tsx; packages/features/clubs/src/join-club-screen.tsxPublic club profile is the key non-member activation surface.
Club mutations and analyticspackages/app/src/presentation/hooks/mutations/use-join-club.ts; use-apply-to-club.ts; use-record-club-growth-event.ts; Supabase club repositories; supabase/migrations/00304_club_join_rpc_hardening.sql; supabase/migrations/00305_signal_immutability_invoker_boundary.sqlDo not bypass repository ports or cache keys. Join policy belongs in RPCs/RLS, not UI-only guards.
Share architecturepackages/app/src/domain/entities/share.entity.ts; packages/app/src/config/kakao-share-templates.json; packages/app/src/ports/services/share.service.port.ts; packages/app/src/adapters/share/*; packages/app/src/navigation/routes.ts; packages/app/src/presentation/utils/share-cards.ts; packages/app/src/presentation/hooks/use-share-*.ts; scripts/check-r1-kakao-share.mjsShare copy belongs in composers/i18n, not inline components. Public share URLs belong in route helpers, not hand-written share-card strings. Kakao template metadata belongs in the manifest plus share composers until a native adapter consumes it.
Public web share routesapps/web/app/s/[sessionId]/*; apps/web/app/m/[matchId]/*; apps/web/app/p/[userId]/*; apps/web/app/lb/[clubId]/*; apps/web/app/.well-known/*/route.ts; apps/web/lib/public-share-preview.ts; apps/web/lib/app-link-association.ts; apps/web/lib/og-card.tsx; supabase/migrations/00306_public_share_previews.sqlPublic previews use a bounded RPC, not broad table RLS. App-link association handlers are credential-gated. Web route changes need receiver smoke/build verification.
Mobile deep-link receiversapps/mobile/app/s/[sessionId].tsx; apps/mobile/app/m/[matchId].tsx; apps/mobile/app/p/[userId].tsx; apps/mobile/app/lb/[clubId].tsx; apps/mobile/app.json; scripts/check-r1-app-links.mjs; apps/mobile/.eas/workflows/maestro-r1-share-deep-links-rerun.ymlEntitlement/config changes are preview-build work, not OTA-only. Static app-link config is guarded by yarn check:r1-app-links; custom-scheme smoke can reuse an existing runtime-compatible preview build.
Scenario proofscripts/run-scenario.mjs; scripts/smoke-r1-share-receivers.mjs; scripts/prepare-r1-share-deep-link-flow.mjs; scripts/check-r1-evidence-workflow.mjs; packages/app/src/presentation/components/dev-panel/scenario-chains.json; packages/app/src/presentation/components/dev-panel/types.ts; supabase/functions/seed-scenario/index.ts; apps/mobile/.maestro/flows/*; apps/mobile/.eas/workflows/*Keep terminal, dev panel, web receiver, custom-scheme receiver, and hosted-smoke claims explicit. 09-club-join-funnels.yaml owns R1-2 join-funnel proof; 10-club-create-first-session-cta.yaml owns R1-2 zero-club create proof; maestro-r1-evidence-rerun.yml owns the final hosted R1 packet.

Test And Smoke Entry Points

Proof needEntry point
Docs/source-pack validityyarn docs:generate; yarn docs:check
Full repo baseline before doneyarn check
Route helper changespackages/app/src/navigation/routes.test.ts through yarn test or yarn check
Share-card URL contractyarn workspace @twomore/app test src/navigation/routes.test.ts src/presentation/utils/__tests__/share-cards.test.ts --runInBand
Club invite-code prefillyarn workspace @twomore/clubs test src/shared/__tests__/join-club-utils.test.ts --runInBand; yarn workspace @twomore/clubs typecheck
Intent-aware onboarding routingyarn workspace @twomore/app test src/navigation/onboarding-intent.test.ts --runInBand
Join hook/adapter contractyarn workspace @twomore/app test src/presentation/hooks/mutations/__tests__/use-join-club.test.ts src/adapters/supabase/__tests__/club.supabase.test.ts --runInBand
Scenario seednode scripts/run-scenario.mjs club_growth
Direct invite-code RPC probeAuthenticated Supabase JS probe calling join_club_by_invite_code('JOINR12'), then node scripts/run-scenario.mjs club_growth to reset fixture membership
Hosted device club-growth smokecd apps/mobile && eas workflow:run .eas/workflows/maestro-e2e.yml
Hosted flow-only reruncd apps/mobile && eas workflow:run .eas/workflows/maestro-e2e-rerun.yml -F build_id=<id>
Local club-growth media smokeyarn e2e:club-growth-media
R1-2 join-funnel smokeyarn e2e:club-join-funnels or hosted 09-club-join-funnels.yaml through the EAS workflow
R1-2 create-first-session smokeyarn e2e:club-create-first-session-cta or hosted maestro-r1-create-club-rerun.yml -F build_id=<id>
R1-3 public receiver smokeyarn smoke:r1-share-receivers --seed --base-url=http://127.0.0.1:3100 against a running web server
Share route web smokeOpen /s/<sessionId>, /m/<matchId>, /p/<userId>, and /lb/<clubId> in the web app; verify metadata/OG after deploy when relevant
Deep-link smokeyarn prepare:r1-share-deep-link-flow, then maestro test apps/mobile/.maestro/generated/r1-share-deep-links.yaml; hosted reuse path: cd apps/mobile && eas workflow:run .eas/workflows/maestro-r1-share-deep-links-rerun.yml -F build_id=<id>
R1 hosted evidence packetyarn check:r1-evidence-workflow, then cd apps/mobile && eas workflow:run .eas/workflows/maestro-r1-evidence-rerun.yml -F build_id=<id> when the preview build/OTA is ready for final R1 packet proof
R1 release readinessyarn check:r1-release-readiness:eas for strict acceptance; yarn report:r1-release-readiness for non-failing handoff while blockers remain
App-link static configyarn check:r1-app-links
App-link remote associationyarn check:r1-app-links:remote; must return direct HTTP 200 JSON from https://twomore.app/.well-known/apple-app-site-association and https://twomore.app/.well-known/assetlinks.json and match credentialed Apple/Android identifiers
OTA compatibilitynode scripts/guard-ota-release.mjs preview; currently expected to block preview OTA until a compatible preview build is created for native-sensitive R1 app-link/Kakao changes and apps/mobile/ota-compatibility-baseline.json is updated
App-link entitlement smokeAfter R1-3 credential setup and preview build, verify https://twomore.app/... opens the installed app on iOS and Android
Kakao template static contractyarn check:r1-kakao-share; credential-required failure mode: yarn check:r1-kakao-share:credentials until all TWOMORE_KAKAO_TEMPLATE_* IDs and either TWOMORE_KAKAO_NATIVE_APP_KEY or EXPO_PUBLIC_KAKAO_APP_KEY exist
Kakao native adapter proofyarn workspace @twomore/app test src/adapters/share/__tests__/share.native.test.ts --runInBand
Kakao-native share smokeAfter credential setup and preview build, verify KakaoTalk renders the intended template and returns users to a useful app/web destination

Version-Control And Update Classification

Use Release Versioning And OTA Workflow for the canonical policy. For R1 work, classify changes this way:

R1 changeGit actionUpdate/build action
This source pack or roadmap docsCommit docs onlyNo OTA
Onboarding route decisions, share-card copy, route helper JS, web fallback behaviorCommit exact source, run yarn checkPreview OTA is allowed only after node scripts/guard-ota-release.mjs preview passes
Web landing/OG route changes onlyCommit exact source, run docs/web checks as applicableWeb deploy/build smoke, no mobile OTA unless client code changed
apps/mobile/app.json associated domains, Android intent filters, Kakao SDK/native module, permissions, runtime versionCommit with app version/runtime decisionPreview build required; do not ship as OTA-only; update the OTA compatibility baseline only after build/install smoke
Supabase migration, seed, or edge-function changes for club_growthCommit exact source and deploy/apply backend change separatelyMobile OTA only after the compatible backend change is applied
Production release promotionCommit and verify from protected main after R4Launch-gated; preview proof first, then guarded production canary/full rollout

Next Implementation Slice

Continue R1-3 Share and deep-link activation from this source pack and Roadmap Implementation Plan.

The next R1-3 implementation conversation should answer:

  1. Which share destinations already have Kakao template metadata, and which Kakao Developers template IDs/native app key values are required before credentialed preview-build proof can start?
  2. Which credentialed app-link smoke should prove the already-wired twomore.app association files and native config on iOS and Android preview builds?
  3. Which share/deep-link items must defer to R4 because they require production domains, Kakao credentials, Apple Associated Domains, or Android App Links?

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