Environment Variables¶
Every env var read by the backend or frontend. Sources: backend/env.example.local, scripts/pull-secrets.sh (authoritative list of prod secrets pulled from GCP Secret Manager), backend/config/config.go, frontend/eas.json, and direct grep of frontend/utils/.
Backend¶
The backend uses a two-layer config: config.{env}.yaml for non-sensitive defaults, .env.{env} for secrets and overrides. Env vars listed here override the matching YAML value. See Configuration for layer precedence.
| Variable | Required? | Default | Purpose |
|---|---|---|---|
ENV |
no | local |
Selects which config.{env}.yaml and .env.{env} to load. Values: local, dev, staging, prod (production is normalized to prod). |
DB_HOST |
yes | from YAML | Postgres host. |
DB_PORT |
yes | from YAML | Postgres port. |
DB_USER |
yes | from YAML | Postgres user. |
DB_PASSWORD |
yes | — | Postgres password. |
DB_NAME |
yes | from YAML | Postgres database name. |
DB_SSLMODE |
yes | from YAML | Postgres SSL mode (disable locally, require in prod). |
REDIS_HOST |
yes | from YAML | Redis host. |
REDIS_PORT |
yes | from YAML | Redis port. |
REDIS_PASSWORD |
no (local) / yes (prod) | — | Redis password. Blank for local docker-compose. |
JWT_SECRET |
yes | — | HMAC secret for signing JWTs. Generate with openssl rand -base64 32. |
ENCRYPTION_KEY |
yes | — | Symmetric key for encrypting provider config (OAuth tokens, etc.) at rest. 32 chars recommended. |
AWS_ACCESS_KEY_ID |
yes | — | S3/MinIO access key for object storage. |
AWS_SECRET_ACCESS_KEY |
yes | — | S3/MinIO secret key. |
S3_ENDPOINT |
no | — | Override endpoint. Set to http://localhost:9000 for local MinIO; leave unset for AWS S3. |
STRIPE_SECRET_KEY |
for payments | — | Stripe API key. Use sk_test_… locally. |
STRIPE_WEBHOOK_SECRET |
for payments | — | Verifies the signature on Stripe webhook callbacks (whsec_…). |
GOOGLE_CLIENT_ID |
for Google OAuth | — | OAuth client ID for Google Sign-In. |
GOOGLE_CLIENT_SECRET |
for Google OAuth | — | OAuth client secret. |
GOOGLE_PLACES_API_KEY |
for Places | — | Google Places API key (autocomplete, place details). |
LINE_CLIENT_ID |
for LINE login | — | LINE Login channel ID. |
LINE_CLIENT_SECRET |
for LINE login | — | LINE Login channel secret. |
APPLE_CLIENT_ID |
for Apple SIWA (iOS) | — | iOS bundle ID, e.g. com.tomoda.app. |
APPLE_SERVICE_ID |
for Apple SIWA (web/Android) | — | Apple Services ID, e.g. com.tomoda.app.service. |
APPLE_TEAM_ID |
for Apple SIWA | — | Apple Developer team ID. |
APPLE_KEY_ID |
for Apple SIWA | — | Apple Sign-in key ID. |
EMAIL_APIKEY |
yes | — | API key for the transactional email provider (Resend). |
SMTP_HOST |
optional | — | SMTP fallback host (e.g. Mailtrap for local testing). |
SMTP_USERNAME |
optional | — | SMTP username. |
SMTP_PASSWORD |
optional | — | SMTP password. |
WEBAUTHN_RP_ID |
yes (passkeys) | from YAML | WebAuthn Relying Party ID — domain only, e.g. localhost or tomoda.life. |
WEBAUTHN_RP_ORIGIN |
yes (passkeys) | from YAML | Full origin including scheme, e.g. http://localhost:3000. |
KLIPY_API_KEY |
for GIF picker | — | Klipy GIF search API key. |
PHOTON_URL |
for self-host geocoder | from YAML | Base URL of the self-hosted Photon (OSM) geocoder. Falls back to disabled if blank. |
FRONTEND_URL |
yes | — | Used to build email verification + password-reset links. |
TWILIO_ACCOUNT_SID |
for SMS | — | Twilio account SID. |
TWILIO_AUTH_TOKEN |
for SMS | — | Twilio auth token. |
TWILIO_PHONE_NUMBER |
for SMS | — | Twilio sender phone number. |
GIN_MODE |
no | release |
Set to debug for verbose Gin logging. |
SERVER_MODE |
no | full |
Selects what the single backend binary starts. Values: full (everything, default for local), multi-hub (API + WS Hub, prod api deployment), async (Asynq worker + scheduler, prod async deployment), api-hub / ws-hub (reserved for the future api/ws split). Overridable per-pod via --mode flag. See Architecture → Decisions. |
Pulling secrets locally
For day-to-day local dev you don't need to copy values into .env.local by hand. Run eval $(./scripts/pull-secrets.sh) to pull from GCP Secret Manager into your shell, then task dev. See scripts.md for the full flow.
Frontend¶
Expo reads EXPO_PUBLIC_* variables at build time and bakes them into the JS bundle. In development they come from frontend/.env. For release builds, the wrapper script at frontend/scripts/build-with-secrets.sh pulls Sentry values from GCP Secret Manager and execs the build command (see scripts.md).
| Variable | Required? | Default | Purpose |
|---|---|---|---|
EXPO_PUBLIC_API_URL |
yes | — | Base URL for the REST API, e.g. http://localhost:8080/api/v1 locally, https://api.tomoda.life/api/v1 in production. |
EXPO_PUBLIC_WS_URL |
yes | — | Base URL for the WebSocket endpoint, e.g. wss://api.tomoda.life/ws. |
EXPO_PUBLIC_WEB_URL |
no | https://tomoda.life |
Marketing/web URL used for share links. |
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID |
yes (Google login) | hardcoded fallback | Google OAuth web client ID. |
EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID |
yes (iOS) | hardcoded fallback | Google OAuth iOS client ID. |
EXPO_PUBLIC_GOOGLE_ANDROID_CLIENT_ID |
yes (Android) | hardcoded fallback | Google OAuth Android client ID. |
EXPO_PUBLIC_APP_VERSION |
no | dev |
App version displayed in the admin dashboard footer. Also sent to Sentry as the release tag. |
EXPO_PUBLIC_COMMIT_SHA |
no | local |
Build commit SHA displayed in the admin dashboard footer. |
EXPO_PUBLIC_BUILD_NUMBER |
no | — | Release build number. Sent to Sentry as the dist tag to disambiguate releases with the same version. |
EXPO_PUBLIC_ENV |
no | dev |
Environment label sent to Sentry (dev / prod). build-with-secrets.sh sets this to prod. |
EXPO_PUBLIC_SENTRY_DSN |
no | — | Sentry project DSN. When empty, Sentry is fully disabled (no init, no captures). Pulled from GCP Secret Manager (tomoda-sentry-dsn) by frontend/scripts/build-with-secrets.sh at release-build time. Required for crash reporting in staging/prod builds — see Frontend → Observability. |
EXPO_PUBLIC_SENTRY_ENV |
no | falls back to EXPO_PUBLIC_ENV |
Optional override if you want a different Sentry environment label from the broader EXPO_PUBLIC_ENV. |
SENTRY_AUTH_TOKEN |
for source-map upload | — | Build-time only, NEVER a public bundle. Auth token used by the Sentry CLI to upload source maps. Pulled from GCP Secret Manager (tomoda-sentry-auth-token) by build-with-secrets.sh. Not prefixed with EXPO_PUBLIC_ precisely because it must not ship to clients. |
SENTRY_ORG |
for source-map upload | tomoda |
Sentry organization slug used by the upload step. Defaults baked into app.config.js — override via env var if your org slug differs. |
SENTRY_PROJECT |
for source-map upload | tomoda-frontend |
Sentry project slug used by the upload step. Defaults baked into app.config.js — override via env var if your project slug differs. |
EXPO_PUBLIC_* is public
Anything prefixed with EXPO_PUBLIC_ is baked into the client bundle and shipped to every user. Treat them as public. Never put secrets, server-only API keys, signing keys, database credentials, or anything you wouldn't paste into a tweet behind an EXPO_PUBLIC_ prefix. If you need a value at runtime that must stay secret, fetch it from the backend over an authenticated channel.
Where values come from in prod¶
Local: .env.local, defaults from pull-secrets.sh.
CI / deploy: Cloud Build pulls from GCP Secret Manager via the tomoda-* secret names — see scripts/pull-secrets.sh for the canonical mapping.
Native release builds: frontend/scripts/build-with-secrets.sh pulls Sentry values from GCP Secret Manager and execs expo run:*. EXPO_PUBLIC_API_URL / EXPO_PUBLIC_WS_URL are hardcoded in frontend/app.config.js's production block.
If you add a new env var: register it in backend/config/config.go::Load, add it to env.example.local, document it here, and (if it's a secret used in dev/prod) add the GCP Secret Manager mapping to scripts/pull-secrets.sh plus the K8s backend-secrets Secret in the devops/ repo.