Architecture
How apps/mobile is laid out - router groups, app-core seams, storage, the theme, and the import rules.
apps/mobile is a thin view layer over the shared packages the web app uses. Screens are React Native; shared behavior lives in @repo/app-core and reaches the app through a seam.
Layout
| Path | Holds |
|---|---|
app/ | Expo Router file routes: (auth) signed-out, (app) signed-in tabs, modals/ the modal stack. |
components/ui/ | React Native Reusables primitives, styled with Uniwind. |
components/<feature>/ | One folder per surface: app shell, auth, home, chat, automations, notifications, settings, billing. |
lib/ | Platform adapters: API client, auth, storage, push, theme. |
providers/ | Root providers and the app-core seam mount. |
hooks/ | Workspace, auth guard, deep-link session, chat, billing status. |
i18n/ | Locale loader and generated message map. |
What apps/mobile may import
apps/mobile/eslint.config.mjs enforces these in bundle files; tests/, e2e/ and the root configs are exempt.
| Specifier | Rule |
|---|---|
@repo/app-core | Allowed. DOM-free, Node-free, framework-free headless behavior. |
@repo/config, @repo/i18n, @repo/utils | Allowed. Pure data and pure functions. |
@repo/config subpaths | A module on a startup path imports @repo/config/mobile, @repo/config/pricing or @repo/config/notification-types, never the root barrel. |
@repo/ai/* subpaths | Allowed, e.g. @repo/ai/ui-message. The bare barrel pulls every provider SDK into the bundle. |
@repo/api, @repo/auth | Type imports only; a value import drags the server into Metro's graph. |
@repo/ui, @repo/database, @repo/payments, next, node:* | Banned. |
Buffer, __dirname, every process member but process.env | Banned globals; Hermes has none. |
Shared LOGIC crosses into this app; shared JSX does not - @repo/ui draws DOM elements React Native cannot render. See client architecture. One primitive adds a rule: components/ui/dialog.tsx needs an intl provider above it, and throws without one.
Seams
@repo/app-core cannot import Expo Router or the auth client, so providers/app-core.tsx mounts this app's slice of each seam once.
| Seam | What the app supplies |
|---|---|
navigation | Expo Router's usePathname() and router.push() |
identity | The signed-in user from the auth client |
notifications | The mobile API client and a native toast sink. Mounted only when config.notifications?.enabled is true, an optional block unlike config.mobile |
Storage
| Kind | Where |
|---|---|
| Session and tokens | expo-secure-store, namespaced by config.mobile.slug |
| Preferences and cached reads | expo-sqlite/kv-store, through app-core's KeyValueStorage seam |
| Pending writes | None. No offline mutation queue: a write needs the network. |
Theme
The shared theme compiles into the generated apps/mobile/global.css, so app and website read one palette. Edit tooling/styles/theme.css, then run pnpm --filter @repo/styles mobile-css. See Theming.
Config
@repo/config/mobile is plain JS with no env reads, because app.config.ts, EAS, Metro and the bundle import it directly.
| Export | Carries |
|---|---|
mobileConfig | App identity (appName, slug, bundleId, scheme), the universal-link host, store URLs, and the push and appLock switches. |
UNIVERSAL_LINK_PATHS, HOME_PATH … SETTINGS_ABOUT_PATH | The web paths the app claims and its own route table - also the allowlist a tapped push is matched against. A route without a constant is no deep-link target. |