Skip to content

Architecture

The frontend is a layered Expo application: a thin route layer at the top, presentational components below it, and a service layer talking to the backend over HTTP and WebSockets. State is shared horizontally through React Context.

Layers

flowchart TB
    subgraph Routes["app/ — Expo Router (file-based)"]
        R1[_layout.tsx<br/>auth gate + providers]
        R2["auth/, home/, discover/,<br/>horizon/, connect/, hub/, ..."]
    end

    subgraph Components["components/"]
        C1[ui/<br/>primitives]
        C2[layout/, chat/, map/,<br/>profile/, modals/, ...]
    end

    subgraph State["contexts/ — React Context"]
        S1[AuthContext]
        S2[ThemeContext]
        S3[FriendsContext]
        S4[LocationContext]
        S5[+ 5 more]
    end

    subgraph Services["services/ + utils/"]
        SV1["10 *Service.ts<br/>(fetch wrappers)"]
        SV2[utils/api.ts<br/>handleResponse]
        SV3[utils/tokenManager.ts<br/>JWT refresh]
        SV4[i18n/<br/>react-i18next]
    end

    subgraph Backend["Backend"]
        B1[REST<br/>/api/v1]
        B2[WebSocket<br/>/ws/chat/:id]
    end

    Routes --> Components
    Routes --> State
    Components --> State
    State --> Services
    Components --> Services
    Services --> Backend
Layer Folder Responsibility
Routes app/ URL → screen mapping, auth gating, layout composition
Components components/ Presentation, interaction, platform splits
State contexts/ Cross-cutting state shared between routes
Services services/ One file per backend domain; pure fetch wrappers
Utilities utils/ HTTP plumbing, token cache, platform helpers
i18n i18n/ Locale files + i18next bootstrap
Constants constants/ Themes, Colors, MapStyles
Hooks hooks/ Reusable hooks (useIsMobile, etc.)

The provider stack

app/_layout.tsx wires every context provider in a fixed order, then renders a <Stack /> from Expo Router:

<ThemeProvider>
  <AuthProvider>
    <ToastProvider>
      <CreateEventProvider>
        <LocationProvider>
          <FriendsProvider>
            <MapDockProvider>
              <SheetProvider>
                <PageHeaderProvider>
                  <RootLayoutNav />

RootLayoutNav then handles auth gating, font loading (@expo-google-fonts/inter), and renders the navigation chrome (mobile bottom dock vs. desktop sidebar via useIsMobile). See State Management for what each provider owns and Routing for the auth gate.

Build setup

Expo config — app.config.js

{
  name: "Tomoda",
  slug: "frontend",
  scheme: "tomoda",
  newArchEnabled: true,
  ios: { bundleIdentifier: "com.tomoda.app", usesAppleSignIn: true,
         associatedDomains: ["webcredentials:tomoda.app", "applinks:app.tomoda.life"] },
  android: { package: "com.tomoda.app", edgeToEdgeEnabled: true },
  plugins: ["expo-router", "expo-apple-authentication", "expo-location"]
}

The new architecture (Fabric / TurboModules) is enabled (newArchEnabled: true). Universal links land on app.tomoda.life. Passkey credentials are scoped to tomoda.app.

Metro

metro.config.js extends the default Expo Metro config. Metro is the JS bundler — it serves the dev bundle and produces the production JS for both native and web targets.

EAS — eas.json

Four build profiles:

Profile Purpose
development-simulator iOS simulator dev client
development Physical-device dev client (internal distribution)
preview Internal distribution (TestFlight / Play internal)
production Public release; injects production EXPO_PUBLIC_*
eas build --platform ios --profile production
eas build --platform android --profile production
eas submit --platform android   # Play Store

Web build

npm run build runs expo export -p web, producing static assets that the frontend Dockerfile serves through nginx (frontend/Dockerfile + frontend/nginx.conf).

Platform split — map example

Tomoda's most invasive cross-platform divergence is the map view:

Platform Library Used in
iOS / Android react-native-maps (Google Maps SDK) components/map/*.{ios,android,tsx}
Web leaflet + react-leaflet (OSM tiles) components/map/*.web.tsx

Metro resolves *.web.tsx for web bundles and *.tsx (or platform-specific *.ios.tsx / *.android.tsx) for native. The same view-model and props are shared; only the rendering primitive differs. See Components.

Cross-cutting concerns

File-size discipline

Project-wide rules (from CLAUDE.md):

  • Keep files under 500 lines.
  • Routes are thin: heavy logic lives in components/ or services/.
  • Each provider owns one concern; reach for a new context only when state truly crosses unrelated subtrees.