Skip to content

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

PatternMeaning
(group)Layout group — organises files without adding a URL segment
[param]Dynamic segment, e.g. profile/[userId].tsx
_layout.tsxWraps everything below it
index.tsxThe 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 stub

Onboarding

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 verification

Completion 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, sessions

Configured 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.tsx

One 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.tsx

conversation/[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].tsx

Presented with presentation: "modal" and a slide-from-bottom animation.

ts
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

  1. Create the file at the path you want the route to be.
  2. If it needs a header, presentation mode, or animation, register it in the nearest _layout.tsx.
  3. Add the data hook under hooks/<domain>/ and the API call under api/<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.

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.

Internal documentation — PlayPalz platform