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¶
- Auth gating: handled centrally in
app/_layout.tsx— see Routing. - HTTP & token refresh: see API Client.
- Real-time chat: see Real-time.
- Theming and primitives: see Design System.
- Translations: see Localization.
- Native release: see Android Setup, Android Release, Building Android.
File-size discipline¶
Project-wide rules (from CLAUDE.md):
- Keep files under 500 lines.
- Routes are thin: heavy logic lives in
components/orservices/. - Each provider owns one concern; reach for a new context only when state truly crosses unrelated subtrees.