Skip to content

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. Used only for resolve escalations (caller-supplied place id, rich-detail enrichment); search and autocomplete are Photon-only.
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.
SYNTHETIC_PROBE_EMAIL no synthetic+probe@tomoda.life Email of the synthetic-monitoring test user seeded at boot by migrate.SeedSyntheticUser (backend/internal/migrate/synthetic_user.go). The Cloudflare synthetic Worker logs in as this user to probe POST /api/v1/auth/login.
SYNTHETIC_PROBE_PASSWORD for login probe Password for the synthetic test user. Empty = seed is skipped (suitable for local dev). In prod, sourced from GCP SM tomoda-synthetic-probe-password via ESO.
SYNTHETIC_PROBE_NAME no Synthetic Probe Display name for the synthetic user.
PHOTON_URL for self-host geocoder from YAML Base URL of the self-hosted Photon (OSM) geocoder. Falls back to disabled if blank.
PHOTON_ALLOW_PRIVATE for dev docker-network false When true, the outbound httpx.SafeClient may reach docker-network hosts (e.g. http://photon:2322). Off in prod (public Photon endpoint).
PHOTON_BIAS_SCALE no 0.2 Photon location_bias_scale for biased forward search: a light nudge toward the viewer, not a radius filter. Matches Photon's public default. A larger value buries distant/prominent results, so autocomplete feels nearby-only.
PHOTON_ZOOM no 14 Photon zoom hint for biased forward search, tuning the scale at which proximity matters.
SERPER_API_KEY for link enrichment Serper.dev key for the link-capture /search + /maps harvest. Empty disables enrichment (capture degrades to the deterministic parse).
ENABLE_SEARCH_ENRICHMENT for link enrichment false Kill-switch for Serper enrichment. Must be exactly true to enable; anything else disables.
CAPTURE_DAILY_PARSE_LIMIT no 50 Per-user/day budget for billable enrichment passes (Redis rate limit). Dev/admin accounts bypass.
ENRICH_DEFAULT_CITY no Ottawa Locale fallback for enrichment when a capture has no resolved place.
ENRICH_DEFAULT_GL no ca Serper country bias (gl) fallback, ISO 3166-1 alpha-2 lowercase.
SEMANTIC_ENABLED no from YAML Master switch for the semantic resolver. Off → enrichment uses the deterministic Serper heuristic only.
SEMANTIC_PROVIDER no openai_compat LLM transport; openai_compat (Ollama / llama.cpp / vLLM / OpenAI-style hosted) is the only value today.
SEMANTIC_BASE_URL for semantic layer from YAML Model server root (OpenAI-compatible chat API). Prod: https://api.deepseek.com. Local Ollama: http://semantic:11434.
SEMANTIC_MODEL for semantic layer from YAML Model id. Prod: deepseek-v4-flash. Local: qwen2.5:3b-instruct.
SEMANTIC_API_KEY for hosted provider Bearer key. In prod, from GCP SM tomoda-deepseek-api-key via ESO. Empty for keyless self-host (Ollama).
SEMANTIC_TIMEOUT no 8s Per-call timeout (runs in the async enrich worker).
SEMANTIC_JSON_MODE no from YAML Request response_format: json_object from the server.
SEMANTIC_ALLOW_PRIVATE for dev docker-network false When true, the outbound httpx.SafeClient may reach docker-network hosts (e.g. http://semantic:11434). Off in prod.
EMBEDDINGS_ENABLED no false Master switch for semantic location search (bge-m3 vectors). Off → search stays lexical-only.
EMBEDDINGS_BASE_URL for semantic search from YAML Text-embeddings (TEI) server root. Prod: the in-cluster TEI service; local: http://localhost:8085.
EMBEDDINGS_MODEL no from YAML Informational model id (bge-m3); the TEI server hosts a single model.
EMBEDDINGS_DIMENSIONS no 1024 Vector length; must match the locations.embedding halfvec column.
EMBEDDINGS_API_KEY for hosted provider Bearer key; empty for keyless self-host (TEI).
EMBEDDINGS_TIMEOUT no 10s Per-call timeout.
EMBEDDINGS_ALLOW_PRIVATE for dev/in-cluster false When true, the outbound httpx.SafeClient may reach private hosts (the TEI service).
ENRICH_BACKGROUND_PAID no true Gates the paid Serper/Places step in the background enrichment worker. task dev sets it false so seeding the catalog locally stays Serper-free (Photon gap-fill + embeddings still run). Interactive resolve/capture paths ignore it. Prod leaves it unset (true).
RANK_WEIGHT_NAME no 0.55 Name-match share of the composite search-ranking score. Unset keeps the shipped value; set to roll a retuned weight without a recompile.
RANK_WEIGHT_PROXIMITY no 0.12 Proximity (distance-decay) share of the search-ranking score.
RANK_WEIGHT_POPULARITY no 0.04 Popularity (saves + external rating) share of the search-ranking score.
RANK_WEIGHT_PROMINENCE no 0.08 Gazetteer-prominence share of the search-ranking score.
RANK_WEIGHT_SOURCE no 0.10 Source-trust (own catalog vs Photon) share of the search-ranking score.
RANK_WEIGHT_SEMANTIC no 0.11 Vector-similarity share of the search-ranking score.
RANK_WEIGHTS_REFRESH_INTERVAL no 2m How often the API ranker re-reads the active ranking_weights row and hot-swaps it in, so an approved retune applies without a redeploy. A Go duration (e.g. 90s, 5m). 0 disables the periodic refresh (boot-resolved weights stay).
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.