{
 "id": "8.6",
 "title": "Secrets and keys",
 "department": "Ops and infrastructure",
 "department_slug": "ops-and-infrastructure",
 "direction": null,
 "branded": false,
 "status": "draft",
 "owner": "Param",
 "last_verified": "Mon Oct 26",
 "holds": "Which secret, where, who rotates it",
 "urls": {
  "html": "https://handbook.joinsquareup.com/ops-and-infrastructure/secrets-and-keys",
  "markdown": "https://handbook.joinsquareup.com/ops-and-infrastructure/secrets-and-keys.md",
  "json": "https://handbook.joinsquareup.com/ops-and-infrastructure/secrets-and-keys.json"
 },
 "other_directions": [],
 "tldr": {
  "headline": "Every key sits in Secret Manager; nobody owns rotation yet",
  "points": [
   {
    "icon": "lock",
    "text": "Services read secrets by name at deploy; production has its own copies of four."
   },
   {
    "icon": "warn",
    "text": "Never rotate the vault key by adding a version alone; it would break stored sign-ins."
   },
   {
    "icon": "flag",
    "text": "Staging, production and the demo share the Anthropic, OpenAI and treg keys."
   },
   {
    "icon": "warn",
    "text": "The default compute account has project-wide secret access, reaching about 15 services."
   },
   {
    "icon": "people",
    "text": "No source names an owner for any secret, so all of them are unowned today."
   }
  ],
  "stat": {
   "value": "15",
   "label": "Services the default account reaches"
  },
  "decide": "Who rotates which secret, and do production keys get split from staging's?",
  "mood": "waiting"
 },
 "sources": [
  "brew origin/main a559de6: deploy.sh:37-42, tools/prod-env.sh:5-12, tools/deploy-demo.sh:15-17, tools/partner-copy.sh:13-16, tools/promote.sh:65-66, db/migrate-to-prod.mjs:19-20, connectors/protected.js:1-4, README.md 'Run it locally'",
  "handbook/comments/server.mjs:1-15, handbook/tools/comments.mjs:1-13, brand-directions/_tools/deploy-handbook.py:61-62",
  "memory brew-prod-backend, brew-apify-watch, brew-pro-switch-and-flush, brew-real-connectors, brew-demo-mode, brew-storage, brand-directions-2026-10-25"
 ],
 "open_questions": [
  "**Who rotates which secret?** No source names anyone. Proposal: Param owns every secret for now; Claude may add versions only when Param asks in chat; a review every quarter starting January 2027.",
  "**Should production have its own AI and data keys?** Today staging, production and the demo share `ANTHROPIC_API_KEY`, `OPENAI_API_KEY` and `TREG_TOKEN`. Proposal: yes for treg and Anthropic before the first paying partner, so a staging leak or runaway test cannot spend production's balance.",
  "**Fix the default compute account's access?** Proposal: yes, remove project-wide secret accessor and give each of the 15 services its own account, in one planned session."
 ],
 "parts": {
  "Answer": "Every key Brew uses lives in Google Secret Manager in project squareup-492617 and reaches a service only by name at deploy time, production has its own copies of the four that protect sign-ins and data, and this page names each secret and where it is used, never its value; who rotates what is still Param's call.",
  "Today": "### Where secrets live\n\n- Google Secret Manager, project squareup-492617. Services read them as `NAME:latest` at deploy. [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10]\n- Production runs as `brew-prod-runtime@`; staging, the demo and the ops jobs run as `brew-runtime@` and cannot read production's secrets or data. [src: brew/tools/prod-env.sh:5-6]\n- Brands' own sign-ins are not secrets in Secret Manager: they are encrypted with the vault key and stored per brand under `private/vault/`. [src: brew/db/connector-ping.mjs:11] [src: memory brew-storage]\n- Locally, keys come from `../ugc-factory/.env`, or the file named in `ENV_FILE`. [src: brew/README.md]\n\n### Secrets by site\n\n| Secret | What it is for | Staging | Production | Demo | Source |\n|---|---|---|---|---|---|\n| `BREW_STAGING_DATABASE_URL` | Staging Postgres | yes | | | [src: brew/deploy.sh:42] |\n| `BREW_PROD_DATABASE_URL` | Production Postgres | | yes | | [src: brew/tools/prod-env.sh:10] |\n| `FEED_SESSION_SECRET` / `_PROD` | Signs sign-in sessions | plain | `_PROD` | plain | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] [src: brew/tools/deploy-demo.sh:17] |\n| `HANDOFF_SECRET` / `_PROD` | Signs hand-off links | plain | `_PROD` | plain | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |\n| `FEED_VAULT_KEY` / `_PROD` | Encrypts each brand's stored sign-ins | plain | `_PROD` | | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |\n| `ANTHROPIC_API_KEY` | Claude | yes | yes | yes | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |\n| `OPENAI_API_KEY` | OpenAI fallback for media | yes | yes | yes | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |\n| `TREG_TOKEN` | treg data calls | yes | yes | yes | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |\n| `YOUTUBE_API_KEY` | YouTube comments for listening | yes | yes | yes | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |\n| `PAGESPEED_API_KEY` | Site speed reads | yes | yes | yes | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |\n| `RESEND_API_KEY` | Team emails | yes | yes | | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |\n| `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` | Google sign-in | yes | yes | yes | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |\n| `GOOGLE_ADS_OAUTH_CLIENT_ID`, `_SECRET`, `GOOGLE_ADS_DEVELOPER_TOKEN` | Google Ads and GA4 connect | yes | yes | | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |\n| `META_ADS_OAUTH_CLIENT_ID`, `_SECRET` | Meta connect | yes | yes | | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |\n| `FEED_SHOPIFY_CLIENT_ID`, `_SECRET` | Shopify app | yes | yes | | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |\n| `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`, `SLACK_SIGNING_SECRET` | Slack app | yes | yes | | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |\n| `APIFY_TOKEN` | Apify, added only if the secret exists | yes | yes | | [src: brew/deploy.sh:38] [src: brew/tools/prod-env.sh:11] |\n| `HF_API_KEY_ID`, `HF_API_KEY_SECRET` | Higgsfield, added only if both exist | yes | yes | | [src: brew/deploy.sh:37] [src: brew/tools/prod-env.sh:12] |\n| `CLOUDFLARE_DNS_ONBOARDING_TOKEN` | Cloudflare DNS changes by script | ops only | | | [src: memory brew-demo-mode] |\n\n### Ops jobs\n\n- `brew-partner-copy` runs as its own service account and reads both vault keys and both database URLs; it refuses to run if the two vault keys are equal. [src: brew/tools/partner-copy.sh:13-16] [src: brew/db/migrate-to-prod.mjs:19-20]\n- `brew-prod-migrate` and `brew-prod-clean` read only `BREW_PROD_DATABASE_URL`. [src: brew/tools/promote.sh:65-66] [src: brew/tools/promote.sh:85-86]\n- `brew-backup` has a custom export-only role. [src: memory brew-prod-backend]\n\n### Known key history\n\n| When | What | Source |\n|---|---|---|\n| 20 Oct | New Anthropic key, `ANTHROPIC_API_KEY` version 3; the key ran out of credit the same morning | [src: memory brew-pro-switch-and-flush] |\n| about 23 Oct | Old treg key died with 401s; `TREG_TOKEN` version 2 is an agent key `brew-live` on team squareup, capped at 20k calls a day; version 1 disabled | [src: memory brew-apify-watch] |\n| 23 Oct | `APIFY_TOKEN` added with the Apify Starter plan | [src: memory brew-apify-watch] |\n| 25 Oct | Production got its own session, vault and hand-off secrets | [src: memory brew-prod-backend] |\n\n### The handbook's own\n\n- Deploys of the docs and the handbook use the person's own gcloud sign-in token, nothing stored. [src: brand-directions/_tools/deploy-handbook.py:61-62]\n- The comments service checks Cloudflare Access's signed identity; it needs `CF_TEAM_DOMAIN`, `CF_ACCESS_AUD` and `ALLOWED_EMAILS`, which are settings, not secrets. It reads Firestore as its runtime account. [src: handbook/comments/server.mjs:1-15]\n- Claude's side reads the same Firestore through Google sign-in on the Mac. [src: handbook/tools/comments.mjs:1-13]\n\n### A known gap\n\nThe default compute service account has project-wide secret access and Cloud Run admin, which reaches about 15 other services. Raised as open for Param on 25 Oct. [src: memory brew-prod-backend]",
  "Tomorrow": "| Item | Version | Tag |\n|---|---|---|\n| Secret Manager, names only in code, production's own copies of the four | v1 | Stays |\n| One named owner and a rotation date per secret | v1 | New |\n| Separate production keys for Anthropic, OpenAI and treg, so spend and leaks are told apart | v2 | New |\n| Default compute account stripped of project-wide secret access | v1 | Changes |",
  "How we do it": "1. Never paste a secret value in chat, code, the handbook or a log. Refer to it by name.\n2. Add a new key:\n\n```bash\nprintf '%s' \"<value>\" | gcloud secrets create <NAME> --data-file=- --project=squareup-492617\n```\n\n3. Rotate a key: add a version, deploy staging, promote production, then disable the old version.\n\n```bash\nprintf '%s' \"<new value>\" | gcloud secrets versions add <NAME> --data-file=- --project=squareup-492617\ngcloud secrets versions disable <old version> --secret=<NAME> --project=squareup-492617\n```\n\n4. Never rotate `FEED_VAULT_KEY` or `FEED_VAULT_KEY_PROD` by adding a version alone: every stored brand sign-in is encrypted with it. A vault key change needs a re-encrypt job like the partner copy, and Arata's sign-ins must survive it. [src: brew/db/migrate-to-prod.mjs:19-20] [src: brew/connectors/protected.js:1-4]\n5. Rotating `FEED_SESSION_SECRET_PROD` signs everyone out of production; tell the partners first.",
  "Numbers": "| Metric | Definition | Where measured |\n|---|---|---|\n| Secrets without an owner | Names in the table with no owner set | This page, Proposed; all of them today |\n| Oldest secret version in use | Days since the serving version was made | `gcloud secrets versions list`, Proposed |",
  "Open": "1. **Who rotates which secret?** No source names anyone. Proposal: Param owns every secret for now; Claude may add versions only when Param asks in chat; a review every quarter starting January 2027.\n2. **Should production have its own AI and data keys?** Today staging, production and the demo share `ANTHROPIC_API_KEY`, `OPENAI_API_KEY` and `TREG_TOKEN`. Proposal: yes for treg and Anthropic before the first paying partner, so a staging leak or runaway test cannot spend production's balance.\n3. **Fix the default compute account's access?** Proposal: yes, remove project-wide secret accessor and give each of the 15 services its own account, in one planned session."
 }
}