Setup
Install the native toolchains, run the mobile app on a simulator or a device, and set the EXPO_PUBLIC_* variables it reads.
Running apps/mobile needs the native toolchains Expo builds against. There is no Expo Go path - the app uses native modules, so the first run compiles a development build.
Prerequisites
| Platform | Install | Notes |
|---|---|---|
| iOS | Xcode with its Command Line Tools | macOS only. Open Xcode once to accept the license and install a simulator runtime. |
| Android | Android Studio, one SDK platform, one emulator image | Set ANDROID_HOME and put platform-tools on your PATH. |
| Android | JDK 17 | Android Studio ships one; java -version must report 17. |
| Both | Node 22.18+ and pnpm | The repo's own engines.node. |
First run
pnpm install.apps/mobile/.env.example to apps/mobile/.env and fill it. Expo reads env from its OWN project root, never the monorepo root.pnpm --filter mobile exec expo run:ios (or expo run:android). The first build takes minutes; later runs reuse it.pnpm --filter mobile mobile:start.Sentry needs nothing before the first build. The Sentry Expo plugin is added only when SENTRY_ORG and SENTRY_PROJECT are both set, so a fresh clone compiles with no symbol-upload build phase to fail on. Set both, plus SENTRY_AUTH_TOKEN, in apps/mobile/.env or your shell when you want source maps and debug symbols uploaded. Expo reads that file before it evaluates app.config.ts, and none of the three is an EXPO_PUBLIC_* key, so none is inlined into the bundle. Runtime error reporting is separate and rides EXPO_PUBLIC_SENTRY_DSN either way.
localhost is the device's loopback, not yours. An Android emulator reaches your machine at 10.0.2.2; a physical device needs your machine's LAN IP. Change EXPO_PUBLIC_API_URL and EXPO_PUBLIC_WEB_URL to match, or every request fails with a network error.
Environment
apps/mobile/.env and .env.example are written by the CLI. Every EXPO_PUBLIC_* value is inlined into the bundle, so none is ever a secret.
| Variable | Purpose |
|---|---|
EXPO_PUBLIC_API_URL | Absolute API base. Fullstack: https://host/api. Separate backend: https://api-host. Fill it first: without it the app throws at launch and names it. |
EXPO_PUBLIC_WEB_URL | Web origin for legal pages and web-only flows. |
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID, EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID | Native Google sign-in. |
EXPO_PUBLIC_SENTRY_DSN, EXPO_PUBLIC_POSTHOG_KEY | Observability. Optional. |
EAS_PROJECT_ID | Read by app.config.ts in Node, never inlined, and not an EXPO_PUBLIC_* app key. Set it in your shell and the EAS environments locally, and as a GitHub repository variable - not a secret - for the release workflow to read. |
apps/mobile/types/env.d.ts names every EXPO_PUBLIC_* key, and tests/env-contract.test.ts holds it and .env.example to the same set both ways. The backend's own server keys are on Environment variables.
On a physical device
pnpm --filter mobile exec eas login. The workspace installs the eas-cli version eas.json requires, so never use a global one.pnpm --filter mobile exec eas device:create.pnpm --filter mobile exec eas build --profile development-device --platform ios (or android).pnpm --filter mobile mobile:start and open the app.Use development-device, not development: the latter sets ios.simulator, so its iOS artifact runs on a simulator and cannot be installed on a phone. Both carry the same dev client and the same development channel. A development build is development-signed, so iOS 16 and later asks for Developer Mode on the phone (Settings -> Privacy & Security). See Release.