Skip to main content

Secrets Management

See also: Local Development · CI CD and Deploys · Feedback System

Where secrets live

Runtime secrets (API keys etc.) live in Firebase Secret Managernot in .secret.local, .env, or system environment variables.

To add or update one (once per environment — dev and prod if needed):

firebase functions:secrets:set SECRET_NAME
# e.g.
firebase functions:secrets:set ORS_API_KEY --project stretched-dev
firebase functions:secrets:set ORS_API_KEY --project stretched-2c16a

Paste the value when prompted. Never write secret values into any file in the repo.

Current secrets

NameUsed for
ORS_API_KEYOpenRouteService geocoding / routing — fallback provider for /geo/search, primary for matrix + directions
GOOGLE_MAPS_API_KEYGoogle Places API (New) Text Search — preferred /geo/search provider; when unset or failing, the function falls back to ORS
RESEND_API_KEYResend email — free tier, 100/day; internal quota capped at 80/day via the email_quota/global Firestore doc (see Feedback System)

GOOGLE_MAPS_API_KEY needs the Places API (New) enabled on the Google Cloud project (billing required) and the key restricted to that API. Because it is bound to the geo function, the secret must exist in Secret Manager before the next Functions deploy.

How the emulator gets them

Nothing extra: bun run setup (tools/setup-env.ts) runs firebase login, and the authenticated Firebase CLI lets the Functions emulator pull secrets from Secret Manager automatically. See Local Development.

CI secrets

GitHub Actions uses separate deploy credentials (service-account JSON files) stored as GitHub repo secrets: FIREBASE_SERVICE_ACCOUNT_DEV and FIREBASE_SERVICE_ACCOUNT. These authorize deploys only — runtime secrets still come from Secret Manager. See CI CD and Deploys.

Hard rules (from the 2026-07-03 postmortem)

A .secret.local key was once committed and had to be rotated. The chain of events:

  1. An echo "..." >> .gitignore appended the .secret.local entry without a trailing newline, silently corrupting the last ignore rule.
  2. The .husky/pre-push hook auto-formats and auto-commits everything (git add -A + chore: prettier auto-format) — which swept the now-untracked-no-longer-ignored secret file into a commit.

Therefore:

  • Never keep secret values in files inside the repo. If a .secret.local exists on disk it is legacy from before the Secret Manager migration — delete it. Its .gitignore entry is a safety net only, and safety nets fail.
  • Never append to .gitignore with >> (or any blind echo). Open the file and add the entry on its own line.
  • Before pushing, check outgoing commits — remember the pre-push hook may have auto-committed files you didn't stage (see Tooling and Scripts).

If a secret does leak into git history: rotate the key immediately; scrubbing history is secondary.