---
id: "8.6"
title: "Secrets and keys"
dept: ops-and-infrastructure
status: draft
owner: Param
branded: false
last_verified: 2026-10-26
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"
---

# Secrets and keys

## 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

- 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]
- 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]
- 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]
- Locally, keys come from `../ugc-factory/.env`, or the file named in `ENV_FILE`. [src: brew/README.md]

### Secrets by site

| Secret | What it is for | Staging | Production | Demo | Source |
|---|---|---|---|---|---|
| `BREW_STAGING_DATABASE_URL` | Staging Postgres | yes | | | [src: brew/deploy.sh:42] |
| `BREW_PROD_DATABASE_URL` | Production Postgres | | yes | | [src: brew/tools/prod-env.sh:10] |
| `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] |
| `HANDOFF_SECRET` / `_PROD` | Signs hand-off links | plain | `_PROD` | plain | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |
| `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] |
| `ANTHROPIC_API_KEY` | Claude | yes | yes | yes | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |
| `OPENAI_API_KEY` | OpenAI fallback for media | yes | yes | yes | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |
| `TREG_TOKEN` | treg data calls | yes | yes | yes | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |
| `YOUTUBE_API_KEY` | YouTube comments for listening | yes | yes | yes | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |
| `PAGESPEED_API_KEY` | Site speed reads | yes | yes | yes | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |
| `RESEND_API_KEY` | Team emails | yes | yes | | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |
| `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` | Google sign-in | yes | yes | yes | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |
| `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] |
| `META_ADS_OAUTH_CLIENT_ID`, `_SECRET` | Meta connect | yes | yes | | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |
| `FEED_SHOPIFY_CLIENT_ID`, `_SECRET` | Shopify app | yes | yes | | [src: brew/deploy.sh:42] [src: brew/tools/prod-env.sh:10] |
| `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] |
| `APIFY_TOKEN` | Apify, added only if the secret exists | yes | yes | | [src: brew/deploy.sh:38] [src: brew/tools/prod-env.sh:11] |
| `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] |
| `CLOUDFLARE_DNS_ONBOARDING_TOKEN` | Cloudflare DNS changes by script | ops only | | | [src: memory brew-demo-mode] |

### Ops jobs

- `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]
- `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]
- `brew-backup` has a custom export-only role. [src: memory brew-prod-backend]

### Known key history

| When | What | Source |
|---|---|---|
| 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] |
| 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] |
| 23 Oct | `APIFY_TOKEN` added with the Apify Starter plan | [src: memory brew-apify-watch] |
| 25 Oct | Production got its own session, vault and hand-off secrets | [src: memory brew-prod-backend] |

### The handbook's own

- 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]
- 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]
- Claude's side reads the same Firestore through Google sign-in on the Mac. [src: handbook/tools/comments.mjs:1-13]

### A known gap

The 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 |
|---|---|---|
| Secret Manager, names only in code, production's own copies of the four | v1 | Stays |
| One named owner and a rotation date per secret | v1 | New |
| Separate production keys for Anthropic, OpenAI and treg, so spend and leaks are told apart | v2 | New |
| 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.
2. Add a new key:

```bash
printf '%s' "<value>" | gcloud secrets create <NAME> --data-file=- --project=squareup-492617
```

3. Rotate a key: add a version, deploy staging, promote production, then disable the old version.

```bash
printf '%s' "<new value>" | gcloud secrets versions add <NAME> --data-file=- --project=squareup-492617
gcloud secrets versions disable <old version> --secret=<NAME> --project=squareup-492617
```

4. 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]
5. Rotating `FEED_SESSION_SECRET_PROD` signs everyone out of production; tell the partners first.

## Numbers

| Metric | Definition | Where measured |
|---|---|---|
| Secrets without an owner | Names in the table with no owner set | This page, Proposed; all of them today |
| 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.
2. **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.
3. **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.
