Skip to content

Routing

Tomoda uses Expo Router — a file-based router built on React Navigation. Files under frontend/app/ become routes; their on-disk shape is the URL shape.

Conventions

File / folder Meaning
app/foo.tsx Route at /foo
app/foo/index.tsx Route at /foo (folder form)
app/foo/_layout.tsx Layout wrapping every child route (renders a <Stack/>, <Tabs/>, etc.)
app/foo/[id].tsx Dynamic segment, accessible via useLocalSearchParams()
app/(group)/... Route group — folder ignored in the URL, used purely to share layout / organize
app/+html.tsx Web-only custom HTML shell (head, meta, viewport)
app/index.tsx The root route /

The root layout app/_layout.tsx wraps every screen with provider context (see State Management) and renders a single <Stack/>.

Top-level route map

flowchart LR
    Root[app/_layout.tsx<br/>providers + auth gate]
    Root --> Auth[auth/]
    Root --> Home[home/]
    Root --> Disc[discover/]
    Root --> Hor[horizon/]
    Root --> Con[connect/]
    Root --> Hub[hub/]
    Root --> Notif[notifications/]
    Root --> Mom[\(moments\)/]
    Root --> Soc[\(social\)/]
    Root --> Adm[\(admin\)/]
    Root --> Idx[index.tsx]
    Root --> Html[+html.tsx]
Route prefix Type Purpose Children
/ (index.tsx) screen Public landing / redirect target
/auth/* stack Sign-in, sign-up, recovery, onboarding login.tsx, register.tsx, forgot-password.tsx, complete-profile.tsx
/home screen Authenticated home (map-first) index.tsx
/discover stack Default landing after login; the discovery feed index.tsx
/horizon/* stack Personal timeline: events, voyages, waypoints index.tsx, events.tsx, voyages.tsx, waypoints.tsx
/connect/* stack Messaging / DMs / new-chat flow index.tsx, inbox.tsx, new.tsx, chat/[id].tsx, chat/draft.tsx, [userId].tsx
/hub/* stack Profile + settings hub index.tsx, profile.tsx, profile-edit.tsx, appearance.tsx, preferences.tsx, account.tsx, friends/, security/
/notifications screen Inbox of activity notifications index.tsx
(moments)/* group Moments capture flow (no URL prefix) capture.tsx, publish.tsx
(social)/* group Public share targets (no URL prefix) events/[id].tsx, users/[id].tsx
(admin)/* group Admin tooling, gated by role manager/dashboard.tsx
+html.tsx web-only Custom <html> shell for the web bundle

auth/

auth/
├── _layout.tsx
├── login.tsx              -> /auth/login
├── register.tsx           -> /auth/register
├── forgot-password.tsx    -> /auth/forgot-password
└── complete-profile.tsx   -> /auth/complete-profile

Supports Email/Password, Google, Apple, Passkey, and OTP-based flows (driven from contexts/AuthContext.tsx).

connect/

connect/
├── _layout.tsx
├── index.tsx              -> /connect
├── inbox.tsx              -> /connect/inbox
├── new.tsx                -> /connect/new
├── [userId].tsx           -> /connect/:userId  (DM landing)
└── chat/
    ├── [id].tsx           -> /connect/chat/:id (active room)
    └── draft.tsx          -> /connect/chat/draft

Chat rooms are opened via the dynamic chat/[id].tsx route; the WebSocket connection is established in that screen — see Real-time.

horizon/

horizon/
├── _layout.tsx
├── index.tsx     -> /horizon         (landing)
├── events.tsx    -> /horizon/events
├── voyages.tsx   -> /horizon/voyages
└── waypoints.tsx -> /horizon/waypoints

hub/

The settings/profile hub:

hub/
├── _layout.tsx
├── index.tsx              -> /hub
├── profile.tsx            -> /hub/profile
├── profile-edit.tsx       -> /hub/profile-edit
├── appearance.tsx         -> /hub/appearance       (theme picker)
├── preferences.tsx        -> /hub/preferences
├── account.tsx            -> /hub/account
├── friends/index.tsx      -> /hub/friends
└── security/
    ├── index.tsx          -> /hub/security
    ├── change-email.tsx   -> /hub/security/change-email
    └── change-password.tsx

(social)/ — public share targets

(social)/
├── events/[id].tsx        -> /events/:id   (no group prefix)
└── users/[id].tsx         -> /users/:id

These routes are public — they're reachable without authentication so they can render share previews and OG metadata. The auth gate explicitly whitelists /events/... and /users/....

(moments)/

(moments)/
├── capture.tsx            -> /capture
└── publish.tsx            -> /publish

(admin)/

(admin)/manager/dashboard.tsx   -> /manager/dashboard

Admin pages are role-gated at the screen level; the route itself is reachable but the screen redirects non-admins.

Auth gate

The root layout (frontend/app/_layout.tsx) runs an effect on every navigation change to enforce three rules:

useEffect(() => {
  if (isLoading) return;
  const inAuthGroup = segments[0] === 'auth';
  const isPublicRoute =
    segments.length === 0 ||
    pathname.startsWith('/events/') ||
    pathname.startsWith('/users/');

  if (!isAuthenticated) {
    if (!inAuthGroup && !isPublicRoute) {
      router.replace('/auth/login');
    }
  } else {
    if (user && (!user.username || !user.date_of_birth)) {
      // profile incomplete → finish onboarding
      const isCompleteProfilePage =
        segments[0] === 'auth' && segments[1] === 'complete-profile';
      if (!isCompleteProfilePage) {
        router.replace('/auth/complete-profile');
      }
    } else if (inAuthGroup || segments.length === 0) {
      router.replace('/discover');
    }
  }
}, [isAuthenticated, segments, isLoading, user]);
Condition Redirect
Not authenticated + not on auth page + not on public share page → /auth/login
Authenticated but username or date_of_birth missing → /auth/complete-profile
Authenticated and on /auth/* or / → /discover

Default landing

The signed-in landing route is /discover, not /home. /home exists but is reached explicitly via the nav dock.

Where to go next