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 Manager — not 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)
LEMON_SQUEEZY_API_KEYLemon Squeezy API — creates checkouts and fetches customer-portal URLs (see Pricing and Subscriptions)
LEMON_SQUEEZY_STORE_IDLemon Squeezy store id passed on every checkout create
LEMON_SQUEEZY_WEBHOOK_SECRETHMAC secret used to verify X-Signature on POST /subscription/webhook
LEMON_SQUEEZY_VARIANT_MAPJSON map { hours|days: { monthly, yearly } } → Lemon Squeezy variant id strings

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.

GitHub Actions also uses cross-repo dispatch tokens for workflows that call out to other repositories. The current one:

NameUsed for
WIKI_DISPATCH_TOKENFine-grained PAT used by .github/workflows/notify-wiki.yml to fire repository_dispatch events at fun-language/Stretched-wiki when wiki/** changes land on main (see Wiki Publishing).

Setup (one-time, Harlo-only — GitHub does not let agents mint PATs):

  1. Create a fine-grained PAT at https://github.com/settings/tokens?type=beta on an account with admin access to fun-language/Stretched-wiki. Repository access: only fun-language/Stretched-wiki. Permissions: Contents: Read and write (the repository_dispatch write happens implicitly once the token can write repo contents; no extra permission is exposed in the fine-grained UI).
  2. Paste the value into the repo secret WIKI_DISPATCH_TOKEN (Settings → Secrets and variables → Actions → New repository secret).
  3. Confirm by pushing a no-op edit to wiki/ on a branch merged to main, then watch the Notify wiki repository workflow in the Actions tab fire and the Stretched-wiki repo receive the wiki-updated event.

Rotation: mint a replacement PAT following the same steps and overwrite the secret. Revoke the old token at https://github.com/settings/tokens after the next dispatch succeeds (so you can verify the new one is wired before retiring the old one). Rotation cadence is Harlo's call — there is no scheduled rotation, but rotate immediately on any access change, leaver, or suspected exposure.

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.