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
| Name | Used for |
|---|---|
ORS_API_KEY | OpenRouteService geocoding / routing — fallback provider for /geo/search, primary for matrix + directions |
GOOGLE_MAPS_API_KEY | Google Places API (New) Text Search — preferred /geo/search provider; when unset or failing, the function falls back to ORS |
RESEND_API_KEY | Resend email — free tier, 100/day; internal quota capped at 80/day via the email_quota/global Firestore doc (see Feedback System) |
LEMON_SQUEEZY_API_KEY | Lemon Squeezy API — creates checkouts and fetches customer-portal URLs (see Pricing and Subscriptions) |
LEMON_SQUEEZY_STORE_ID | Lemon Squeezy store id passed on every checkout create |
LEMON_SQUEEZY_WEBHOOK_SECRET | HMAC secret used to verify X-Signature on POST /subscription/webhook |
LEMON_SQUEEZY_VARIANT_MAP | JSON map { hours|days: { monthly, yearly } } → Lemon Squeezy variant id strings |
GOOGLE_MAPS_API_KEYneeds the Places API (New) enabled on the Google Cloud project (billing required) and the key restricted to that API. Because it is bound to thegeofunction, 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:
| Name | Used for |
|---|---|
WIKI_DISPATCH_TOKEN | Fine-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):
- 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: onlyfun-language/Stretched-wiki. Permissions:Contents: Read and write(therepository_dispatchwrite happens implicitly once the token can write repo contents; no extra permission is exposed in the fine-grained UI). - Paste the value into the repo secret
WIKI_DISPATCH_TOKEN(Settings → Secrets and variables → Actions → New repository secret). - Confirm by pushing a no-op edit to
wiki/on a branch merged tomain, then watch theNotify wiki repositoryworkflow in the Actions tab fire and theStretched-wikirepo receive thewiki-updatedevent.
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:
- An
echo "..." >> .gitignoreappended the.secret.localentry without a trailing newline, silently corrupting the last ignore rule. - The
.husky/pre-pushhook 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.localexists on disk it is legacy from before the Secret Manager migration — delete it. Its.gitignoreentry is a safety net only, and safety nets fail. - Never append to
.gitignorewith>>(or any blindecho). 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.