Navigation & Routes
expo-router. The file tree under apps/mobile/app/ is the route table — there is no route config to keep in sync.
Conventions
| Pattern | Meaning |
|---|---|
(group) | Layout group — organises files without adding a URL segment |
[param] | Dynamic segment, e.g. profile/[userId].tsx |
_layout.tsx | Wraps everything below it |
index.tsx | The route for the directory itself |
Top level
app/
├── _layout.tsx root providers
├── (auth)/ unauthenticated
├── (onboarding)/ post-registration setup
└── (app)/ authenticated product(app)/_layout.tsx is the gate: unauthenticated users are redirected to (auth), and users who have not finished setup to (onboarding).
Authentication
(auth)/
├── index.tsx landing / choose sign-in
├── login.tsx
├── register.tsx
├── forgot-password.tsx ⚠ backend is a stub
└── reset-password.tsx ⚠ backend is a stubOnboarding
An ordered flow, one decision per screen:
(onboarding)/
├── index.tsx start
├── birthdate.tsx age gate
├── avatar.tsx
├── bio.tsx
├── games.tsx favourite games → UserGame
├── creator-choice.tsx fan or creator?
├── creator-intro.tsx creator explainer
├── pii.tsx personal details (creators)
├── id-capture.tsx identity document
├── selfie.tsx liveness
└── review-pending.tsx awaiting verificationCompletion sets User.onboarded, which is what the (app) gate checks. The creator branch (creator-choice onward) sets userType = "playpal" and feeds identity verification.
Tabs
(app)/(tabs)/
├── home/ the feed
├── explore/ discovery and trending
├── chat/ conversations and channels
├── shop/ the store
└── me/ profile, settings, sessionsConfigured in (tabs)/_layout.tsx with Feather icons and the Outfit font. The tab bar is 80pt tall with explicit padding — deliberate, to sit above the home indicator.
me/
The deepest subtree, split between profile editing and settings:
me/
├── index.tsx
├── edit/
│ ├── index.tsx username.tsx name.tsx bio.tsx
│ ├── links.tsx add-link.tsx edit-link.tsx
│ ├── subscription-price.tsx
│ └── working-hours.tsx
├── sessions/
│ ├── index.tsx
│ └── [sessionId].tsx
└── settings/
├── index.tsx security.tsx privacy.tsx content.tsx
├── change-password.tsx active-sessions.tsx login-history.tsx
├── notifications.tsx appearance.tsx streaming.tsx
├── blocked-users.tsx delete-account.tsx
├── help.tsx terms.tsx privacy-policy.tsxOne field per screen under edit/ mirrors the API, which exposes a separate endpoint per profile field.
Stack screens
Pushed over the tabs, so they keep the tab bar's context:
(app)/
├── post/[postId].tsx modal, slide from bottom
├── profile/[userId].tsx
├── channel/[channelId].tsx
├── room/[roomId].tsx voice / video room
├── conversation/[userId].tsx DM thread
├── live-stream/[livestreamId].tsx modal
└── notifications/index.tsxconversation/[userId] is keyed by user id, not conversation id — the screen calls POST /conversation to create-or-get the thread. That is why "message this person" works from anywhere without knowing whether a conversation already exists.
Modals
(app)/(modals)/
├── post/create.tsx
├── subscribe/[userId].tsx
├── manage-subscription/[userId].tsx
├── schedule/[userId].tsx book a session
├── buy-tokens/index.tsx Play Coins
├── paywall/index.tsx platform subscription
└── product/[productId].tsxPresented with presentation: "modal" and a slide-from-bottom animation.
Navigating
import { router } from "expo-router";
router.push("/profile/clx123");
router.push({ pathname: "/conversation/[userId]", params: { userId } });
router.back();
router.replace("/(auth)/login");lib/navigation.ts holds shared helpers — notably the mapping from a notification's deep-link target to a route, so { screen: "post", postId } from a push payload resolves to /post/<postId>. See Notifications.
Adding a screen
- Create the file at the path you want the route to be.
- If it needs a header, presentation mode, or animation, register it in the nearest
_layout.tsx. - Add the data hook under
hooks/<domain>/and the API call underapi/<domain>.ts.
You do not register the route anywhere else. That is the whole point of file-based routing — and the reason a stray file in app/ becomes a live route.
Deep links
The URL scheme comes from EXPO_PUBLIC_ENV (playpalz, playpalz-staging, playpalz-dev), so a link like playpalz://post/clx123 opens that post. Push notifications carry the target params from buildTarget() on the server.
