There are two env files, both gitignored, both created for you by node scripts/init-env.mjs.
apps/mobile/.env — the app
Everything here is prefixed EXPO_PUBLIC_ and is inlined into the JS bundle at build time, so treat these as publishable, not secret.
EXPO_PUBLIC_API_URL=http://localhost:3000
EXPO_PUBLIC_AUTH_PROVIDER=custom-jwt # custom-jwt | clerk | better-auth
EXPO_PUBLIC_AUTH_ENABLED=true # false = no-login app
# Optional integrations — leave blank to no-op:
EXPO_PUBLIC_POSTHOG_KEY=
EXPO_PUBLIC_SENTRY_DSN=
EXPO_PUBLIC_REVENUECAT_IOS_KEY=Env changes need a restart
EXPO_PUBLIC_* values are baked in at bundle time. After editing them, restart the Expo dev server — hot reload won't pick them up.
apps/api/.env — the backend
Server secrets that never ship to the client:
JWT_SECRET=… # generated by init-env
ACCESS_TOKEN_TTL=900
REFRESH_TOKEN_TTL=2592000
DATABASE_URL=… # SQLite by default
ALLOWED_ORIGINS=…
RESEND_API_KEY= # password-reset emails; optionalThe "no key required" rule
Every paid integration — Sentry, PostHog, RevenueCat, Stripe, Clerk, Resend, Google Maps — no-ops when its key is missing. That's deliberate: the app runs green on a fresh clone, and you add keys one integration at a time.
Testing on a real device
Set EXPO_PUBLIC_API_URL=http://<your-LAN-IP>:3000 and restart the dev server so your phone can reach the backend.