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:
- Roadmap for the R1 outcome and sequencing.
- Roadmap Implementation Plan for slice status and verification ladders.
- App Functionality Map for the broader tab and backend capability map.
- Release Versioning And OTA Workflow before publishing any mobile update from R1 work.
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:
| Signal | Intent | Post-onboarding destination |
|---|---|---|
| No route or intent signal | new_player | routes.home |
intent=club_member, /clubs, or /clubs/discover | club_member | routes.discoverClubs |
intent=club_admin or /create-club | club_admin | routes.createClub |
intent=pickup_host or /create-session?clubId=pickup | pickup_host | routes.createSession('pickup') |
intent=invitee or /join-club | invitee | routes.joinClub |
Public share receiver /s, /m, /p, or /lb | invitee | Original public receiver path |
Safe internal next=/... | return_to_link or inferred action intent | Original 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 --runInBandyarn 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)andpublic.join_club_by_invite_code(code, idempotency_key)are added in migration00304_club_join_rpc_hardening.sql.00305_signal_immutability_invoker_boundary.sqlfixes 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_membersis removed from RLS; admins can still add members, and join flows use RPCs. useJoinClubdelegates invite-code joins to the invite RPC.useJoinOpenClubdelegates public-profile open joins to the open-join RPC.- Public club profile now routes
openclubs through direct join and then club detail;invite_onlyclubs still route to/join-club. - Open-club public profile navigation now observes the open-join mutation success state directly, so a successful
join_open_clubresult 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
BottomCtaBandasclub.public-profile.application-submitwith 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-shortcutbeside 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_growthnow contains a repeatableJOINR12invite-code fixture and prunes fixture memberships back to the expected stable member set before every seed run.09-club-join-funnels.yamlcovers invite-code join, open-club direct join, and approval-request submission.10-club-create-first-session-cta.yamlcovers 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 --linkedapplied00304_club_join_rpc_hardening.sqland00305_signal_immutability_invoker_boundary.sqlto the linked preview project.yarn supabase migration list --linkedshows local and remote both at00305.yarn supabase functions deploy seed-scenariodeployed the updatedclub_growthfixture-prune and stable member-selection logic to the linked preview project.node scripts/run-scenario.mjs club_growthpassed withusers=29,clubs=9,sessions=0,memberships=134,media=10,joinRequests=7,prunedMemberships=0, andfixturePrunedMemberships=0in steady state.- Direct authenticated RPC probe passed for
join_club_by_invite_code('JOINR12'), returning one activememberrow. - Direct authenticated RPC probe passed for open join on
성동 베어본 테니스, returning one activememberrow. - Preview fixture count after reset:
JOINR12club is back to8/20active 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, platformsandroidandios. - 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
7c0bd42756e424f55518d58a874a0c34093a9d7dis pushed toorigin/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-d31d22a43a43passed enabled flows01-login,02-main-tabs-smoke,08-club-growth-media, and09-club-join-funnelsusing Android preview build6aa365f6-7814-4250-a708-a27843a919dc. - Targeted hosted workflow
019f0bb1-2f5f-7845-bc8b-3d8ab65bbdfereused the same Android preview build after preview OTAff478b2e, seededr1_create_club_zero, and passed10-club-create-first-session-cta.yaml. Artifacts: JUnit report019f0bb5-212d-7c29-ae01-2c7b796d8360; Maestro results019f0bb5-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.tsnow owns public share route helpers:publicSessionShare,publicMatchShare,publicProfileShare,publicLeaderboardShare, andpublicClubInviteShare.buildSessionShareCard,buildMatchShareCard,buildStatCardShareCard,buildLeaderboardShareCard, andbuildClubInviteShareCardnow 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.tsxandapps/web/app/join-club/page.tsxread thecodequery and pass it into the sharedJoinClubScreen.JoinClubScreennormalizes linked and typed invite codes throughnormalizeInviteCodeInput, 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.sqladdspublic.get_public_share_preview(kind, id), aSECURITY DEFINERRPC that exposes only the small public preview field set for sessions, matches, profiles, and discoverable club leaderboards.- Public web receiver pages and
opengraph-imagehandlers for/s,/m,/p, and/lbnow read throughapps/web/lib/public-share-preview.tsinstead of unauthenticated table queries. OpenInAppButtonis server-rendered and exposes a realtwomore://...anchor in HTML, so the web fallback is visible without hydration while true universal/app links remain a separate native-build gate.club_growthnow returns deterministic share fixture IDs:shareClubId,shareSessionId,shareMatchId,shareProfileId, andshareInviteCode.yarn smoke:r1-share-receivers --seed --base-url=http://127.0.0.1:3100signs 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.mjssigns in with dev credentials, invokes deployedclub_growth, readsshareProfileId,shareSessionId,shareClubId,shareMatchId, andshareInviteCodefrom the seed summary, and writes ignored generated YAML atapps/mobile/.maestro/generated/r1-share-deep-links.yaml.yarn prepare:r1-share-deep-link-flowprepares the generated flow;yarn e2e:r1-share-deep-linksprepares it and runs Maestro locally when the CLI and a preview build are available.apps/mobile/.eas/workflows/maestro-r1-share-deep-links-rerun.ymlreuses an existing Android preview build, seeds/generates the flow inbefore_maestro_tests, and runs only the generated R1-3 custom-scheme flow.scripts/prepare-r1-share-deep-link-flow.mjsnow asserts the first-viewport public-club가입하기CTA after the/lbreceiver, avoiding the stale below-the-fold소식tab assertion that can fail after a correct route land.scripts/prepare-r1-share-deep-link-flow.mjsalso openstwomore://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.jsondefines the R1 Kakao custom-template manifest forsession_invite,club_invite,match_result,tier_promotion,stat_card, andleaderboard.ShareContentnow carries optionalkakaoTemplatemetadata, 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.mjsverifies manifest coverage, share-card references, package script wiring, iOS Kakao schemes, and native-module state. The normalyarn check:r1-kakao-sharemode passes static source coverage and reports missing external gates; credential modeyarn check:r1-kakao-share:credentialsintentionally fails untilTWOMORE_KAKAO_TEMPLATE_*IDs and a Kakao native app key are available.- Source commit
422037f28e815f17c179f54d2a033ac40bda3c60is 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/coreand@react-native-kakao/shareare installed in the mobile app shell and exposed to@twomore/appthrough the existing peer/development dependency pattern.apps/mobile/app.config.jsmaps configuredTWOMORE_KAKAO_TEMPLATE_*IDs into Expoextraand conditionally applies the@react-native-kakao/coreconfig plugin whenTWOMORE_KAKAO_NATIVE_APP_KEYis present, falling back toEXPO_PUBLIC_KAKAO_APP_KEYwhen that existing login variable carries the same Kakao Native App Key.packages/app/src/adapters/share/share.native.tsnow attemptsshareCustomTemplatewhen a share card haskakaoTemplatemetadata and a valid template ID. Missing IDs, missing native linkage, and native SDK errors fall back to React NativeShare.share.packages/app/src/adapters/share/__tests__/share.native.test.tscovers Kakao success, missing-template fallback, native failure fallback, and generic non-Kakao share content.scripts/check-r1-kakao-share.mjsnow 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 eitherTWOMORE_KAKAO_NATIVE_APP_KEYorEXPO_PUBLIC_KAKAO_APP_KEYexist.- Source commit
fb2fd1bd76835a6921f1623b8c9600da2b3f6796is 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-185df88e8ad4passed against Android preview build6aa365f6-7814-4250-a708-a27843a919dc. The pass coveredtwomore://p,twomore://s,twomore://lb, andtwomore://mwith seededclub_growthshare IDs. - Hosted generated invite-link proof:
019f0c63-85bc-7f19-9d9b-d449ed548eb1passed against the same Android preview build. The pass coveredtwomore://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
| Step | Current behavior | Source surfaces | Status |
|---|---|---|---|
| Onboarding completion | The 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.ts | Built, intent-aware on mobile |
| Club discovery | The 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.tsx | Built |
| Create club | The 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.yaml | Built; hosted 10 green |
| Join open club | Public 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.sql | Direct 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.sql | Direct RPC green; hosted 09 green; prefill tests green |
| Apply to approval club | Public 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.ts | Built; hosted 09 green for seeded request submission |
| Session interest | Public 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.ts | Built |
| Session RSVP | Session 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.ts | Built, R1 proof incomplete |
| Share | Share 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.mjs | Built 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 landing | Web 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.mjs | Built 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 smoke | Hosted 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.mjs | 01/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 family | Mobile routes | Web routes | Purpose |
|---|---|---|---|
| Home | apps/mobile/app/(tabs)/(home)/index.tsx | apps/web/app/(tabs)/home/page.tsx | Default post-onboarding destination and daily attention router |
| Clubs | apps/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/create | apps/mobile/app/join-club.tsx; apps/mobile/app/create-club.tsx | apps/web/app/join-club/page.tsx; apps/web/app/create-club/page.tsx | Invite-code join and club creation |
| Sessions | apps/mobile/app/clubs/[clubId]/sessions/*; apps/mobile/app/sessions/[sessionId]/* | apps/web/app/clubs/[clubId]/sessions/page.tsx; session routes through shared screens where available | Session detail, RSVP, attendance, host/admin work |
| Public share | 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/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 | Link recipients can preview on web or open the installed app |
| Leaderboard/profile | apps/mobile/app/leaderboard.tsx; apps/mobile/app/profile/[userId].tsx; apps/mobile/app/records/leaderboard.tsx | apps/web/app/leaderboard/page.tsx; apps/web/app/profile/[userId]/page.tsx; apps/web/app/records/leaderboard/page.tsx | Shared 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 ID | Source-backed rule | Applicability to TwoMore | Current state |
|---|---|---|---|
| R1-E1 | HTTP 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-E2 | Custom 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-E3 | Rich 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-E4 | KakaoTalk 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-E5 | EAS 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-E6 | Expo 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-E7 | Expo 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-E8 | Join 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
| Gap | Why it matters | Next slice |
|---|---|---|
| Kakao-native delivery is not proven | Korean 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 proof | Static/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 proof | The 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 layers | scripts/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 proven | Product 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.
| Surface | Primary paths | Notes |
|---|---|---|
| Onboarding | apps/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.ts | Keep consent/under-14 gates intact; intent routing belongs in the root gate and pure resolver, not inside consent/profile writes. |
| Club discovery and join | packages/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.tsx | Public club profile is the key non-member activation surface. |
| Club mutations and analytics | packages/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.sql | Do not bypass repository ports or cache keys. Join policy belongs in RPCs/RLS, not UI-only guards. |
| Share architecture | 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/*; 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.mjs | Share 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 routes | apps/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.sql | Public 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 receivers | 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.json; scripts/check-r1-app-links.mjs; apps/mobile/.eas/workflows/maestro-r1-share-deep-links-rerun.yml | Entitlement/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 proof | scripts/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 need | Entry point |
|---|---|
| Docs/source-pack validity | yarn docs:generate; yarn docs:check |
| Full repo baseline before done | yarn check |
| Route helper changes | packages/app/src/navigation/routes.test.ts through yarn test or yarn check |
| Share-card URL contract | yarn workspace @twomore/app test src/navigation/routes.test.ts src/presentation/utils/__tests__/share-cards.test.ts --runInBand |
| Club invite-code prefill | yarn workspace @twomore/clubs test src/shared/__tests__/join-club-utils.test.ts --runInBand; yarn workspace @twomore/clubs typecheck |
| Intent-aware onboarding routing | yarn workspace @twomore/app test src/navigation/onboarding-intent.test.ts --runInBand |
| Join hook/adapter contract | yarn workspace @twomore/app test src/presentation/hooks/mutations/__tests__/use-join-club.test.ts src/adapters/supabase/__tests__/club.supabase.test.ts --runInBand |
| Scenario seed | node scripts/run-scenario.mjs club_growth |
| Direct invite-code RPC probe | Authenticated 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 smoke | cd apps/mobile && eas workflow:run .eas/workflows/maestro-e2e.yml |
| Hosted flow-only rerun | cd apps/mobile && eas workflow:run .eas/workflows/maestro-e2e-rerun.yml -F build_id=<id> |
| Local club-growth media smoke | yarn e2e:club-growth-media |
| R1-2 join-funnel smoke | yarn e2e:club-join-funnels or hosted 09-club-join-funnels.yaml through the EAS workflow |
| R1-2 create-first-session smoke | yarn e2e:club-create-first-session-cta or hosted maestro-r1-create-club-rerun.yml -F build_id=<id> |
| R1-3 public receiver smoke | yarn smoke:r1-share-receivers --seed --base-url=http://127.0.0.1:3100 against a running web server |
| Share route web smoke | Open /s/<sessionId>, /m/<matchId>, /p/<userId>, and /lb/<clubId> in the web app; verify metadata/OG after deploy when relevant |
| Deep-link smoke | yarn 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 packet | yarn 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 readiness | yarn check:r1-release-readiness:eas for strict acceptance; yarn report:r1-release-readiness for non-failing handoff while blockers remain |
| App-link static config | yarn check:r1-app-links |
| App-link remote association | yarn 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 compatibility | node 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 smoke | After R1-3 credential setup and preview build, verify https://twomore.app/... opens the installed app on iOS and Android |
| Kakao template static contract | yarn 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 proof | yarn workspace @twomore/app test src/adapters/share/__tests__/share.native.test.ts --runInBand |
| Kakao-native share smoke | After 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 change | Git action | Update/build action |
|---|---|---|
| This source pack or roadmap docs | Commit docs only | No OTA |
| Onboarding route decisions, share-card copy, route helper JS, web fallback behavior | Commit exact source, run yarn check | Preview OTA is allowed only after node scripts/guard-ota-release.mjs preview passes |
| Web landing/OG route changes only | Commit exact source, run docs/web checks as applicable | Web 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 version | Commit with app version/runtime decision | Preview 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_growth | Commit exact source and deploy/apply backend change separately | Mobile OTA only after the compatible backend change is applied |
| Production release promotion | Commit and verify from protected main after R4 | Launch-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:
- 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?
- Which credentialed app-link smoke should prove the already-wired
twomore.appassociation files and native config on iOS and Android preview builds? - Which share/deep-link items must defer to R4 because they require production domains, Kakao credentials, Apple Associated Domains, or Android App Links?