Adopt Cloudflare Secrets Store when GA
ADR-025: Adopt Cloudflare Secrets Store when GA
Status: Accepted (2026-04-24) — migration blocked on Secrets Store GA Supersedes: none Superseded-by: none
Context
Today V5 manages ~25 provider credentials (HubSpot, Salesforce, Google OAuth,
Slack, Gong, DeepSeek, OpenAI, Anthropic, Cerebras, Groq, OpenRouter, SEMrush,
AWS per-service IAM users, Apollo, etc.) via per-Worker wrangler secret put.
That works for a single Worker but is already fraying at the edges:
- Adding the scheduler Worker, preview Workers, a dev Worker, or the context
worker means duplicating every secret across each
wrangler.toml. Rotation becomes N-way. - Rotation requires a deploy per Worker. During a compromise, that’s unacceptable friction.
- There’s no account-level audit or listing; visibility is per-Worker.
Cloudflare announced Secrets Store (overview page: https://developers.cloudflare.com/secrets-store/) — open beta as of 2026-04-16. Relevant properties:
| Attribute | wrangler secret put | Secrets Store |
|---|---|---|
| Scope | Per-Worker | Per-account (one store, many bindings) |
| Rotation | Manual per Worker + redeploy | Rotate once, all bound Workers see new value (no redeploy) |
| Access in code | env.SECRET_NAME synchronous | await env.BINDING.get() async |
| Dashboard visibility | Per-Worker | Account-wide listing + audit |
| Reuse across Workers | Duplicate per Worker | Single source of truth |
| AI Gateway BYOK | Not supported | Native integration |
wrangler.toml binding shape (from the docs):
[[secrets_store_secrets]]binding = "MY_BINDING"store_id = "<STORE_ID>"secret_name = "MY_SECRET"Worker access pattern:
// Async .get() on every request — one awaitable per secret touchedconst apiKey = await env.HUBSPOT_TOKEN.get();Research receipts (2026-04-24):
- Overview + beta status: https://developers.cloudflare.com/secrets-store/
- Workers integration: https://developers.cloudflare.com/secrets-store/integrations/workers/
Decision
Adopt Cloudflare Secrets Store for all V5 + Context Worker secrets within 30 days of the Secrets Store GA announcement.
Until GA:
- Continue using
wrangler secret putper Worker. - Follow the rotation procedure in
docs/runbooks/secret-rotation.mdon a quarterly cadence and immediately on any suspected compromise. - Keep the migration plan (below) current so the refactor is mechanical the day GA drops.
Rationale
Why adopt
- Rotation-in-place is the feature we bleed for.
wrangler secret putrotation is a 5-minute-per-Worker chore that compounds linearly as we add Workers. One-store rotation scales. - Account-wide audit closes a real compliance gap — we can’t today show a client “here is every credential we hold for you and the last time it was touched.”
- AI Gateway BYOK (Task 3.2) natively consumes Secrets Store — keeping provider keys one hop away from the gateway.
Why wait for GA
- Still open beta as of 2026-04-16. No SLA. Production credential plane on a beta is a real risk: if the backend has an incident, every Worker’s hot path breaks until the fetch recovers.
- The
await env.BINDING.get()async-on-read pattern is a non-trivial refactor across 50+ provider adapters (every tool usingresolveTokenData(), everyenv.XXX_API_KEYsynchronous read). It’s boring and mechanical — but it has to be done in ONE sweep so we don’t mixenv.X(sync) andawait env.X.get()(async) across the codebase.
Why not partial adoption now (e.g. only new secrets)
Creates a dual-world problem: one list of secrets in wrangler, another in the Store. That doubles the audit surface — the opposite of the goal. Wait, then do the sweep in one go.
Migration trigger
“Within 30 days of the Cloudflare Secrets Store GA announcement.”
When GA lands:
- Create the store:
wrangler secrets-store store create ascend-prod. - Import every secret via
wrangler secrets-store secret put. - Swap
wrangler.toml— replace each bare secret reference with a[[secrets_store_secrets]]block. One-line change per secret. - Refactor every
env.XXX_API_KEYcall site toawait env.XXX_API_KEY.get(). There’s a deterministic pattern here — a codemod can cover 95% of sites. - Update adapters’
resolveTokenData()to await once per request and reuse within the same request context (same memoization pattern we already use for D1 / KV reads). - Run the full test suite + staging canary + production canary before
removing the
wrangler secret putfallback paths. - Archive the migration as its own ADR closing this one out.
Consequences
Short-term (pre-GA): slight operational burden — manual rotation continues; every new Worker duplicates secrets. Documented + time-boxed.
Post-GA: one-time refactor of ~50 call sites. After that, rotation becomes a 30-second dashboard action and every new Worker just binds what it needs.
Risk: delay GA announcement → delay burden compounding. Reassess at 90 days — if no GA by then, reopen this ADR to consider a phased partial adoption (new-secrets-only in the Store, existing ones stay in wrangler) as a lesser-of-two-evils bridge.
Related
- Wave 3 Task 3.8 in
docs/architecture/ASCEND-CLOUD-NATIVE-V2-ENGINEERING-PLAN.md - Rotation procedure:
docs/runbooks/secret-rotation.md - AI Gateway Task 3.2 (prefers Secrets-Store-backed BYOK post-GA):
src/lib/ai-gateway.ts