Adopt Cloudflare Secrets Store when GA
ADR-025: Adopt Cloudflare Secrets Store when GA
Section titled “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
Section titled “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
Section titled “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
Section titled “Rationale”Why adopt
Section titled “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
Section titled “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)
Section titled “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
Section titled “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
Section titled “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
Section titled “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