Secrets Store & auth tokens
Cloudflare’s Secrets Store is an account-level container for secrets. Unlike a plain env var, a stored secret is shared across Workers, redacted in the dashboard, and read live at runtime — bound Workers see the current value without a redeploy.
In this guide you’ll build a bearer-token-protected Worker: a
Store, a token minted with Alchemy.Random, a Secret holding it,
and a ReadSecret binding that checks it on every request.
Create the store
Section titled “Create the store”import * as Cloudflare from "alchemy/Cloudflare";
export const Store = Cloudflare.SecretsStore.Store("AuthSecrets");Cloudflare allows one Secrets Store per account, so the provider
adopts the existing store if one exists (creating it only on a truly
fresh account) and never deletes it on alchemy destroy — it’s
account-level infrastructure that outlives any single stack.
Generate a stable token
Section titled “Generate a stable token”import { Random } from "alchemy";
export const AuthTokenValue = Random("AuthTokenValue");Random mints a random value once (32 bytes, hex-encoded — pass
bytes to change the length) and persists it in state, so the token
stays the same on every subsequent deploy. Its text attribute is a
Redacted<string>, so it never leaks into logs.
Put it in the store
Section titled “Put it in the store”import * as Effect from "effect/Effect";
export const AuthToken = Effect.gen(function* () { const store = yield* Store; const value = yield* AuthTokenValue; return yield* Cloudflare.SecretsStore.Secret("AuthToken", { store, value: value.text, });});Secret writes the value into the store under the resource’s logical
ID (pass name to override). The value is any Redacted<string> —
Redacted.make(process.env.API_KEY!) works just as well as a
Random output. Changing the value updates the secret in place;
renaming it or moving it to another store replaces it.
Read it in a Worker
Section titled “Read it in a Worker”Bind the secret in the Worker’s init phase with ReadSecret, then
check the Authorization header in fetch:
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import * as Redacted from "effect/Redacted";import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";import { AuthToken } from "./auth.ts";
export default class Api extends Cloudflare.Worker<Api>()( "Api", { main: import.meta.url, }, Effect.gen(function* () { const authToken = yield* Cloudflare.SecretsStore.ReadSecret(AuthToken);
return { fetch: Effect.gen(function* () { const request = yield* HttpServerRequest;
if (request.url.startsWith("/protected")) { const authHeader = request.headers.authorization; const expected = yield* authToken; if ( !authHeader || authHeader !== `Bearer ${Redacted.value(expected)}` ) { return HttpServerResponse.text("unauthorized", { status: 401 }); } return HttpServerResponse.text("ok"); }
return HttpServerResponse.text("public"); }).pipe( Effect.catchTag("SecretError", (err) => Effect.succeed( HttpServerResponse.text(`failed to read secret: ${err.message}`, { status: 500, }), ), ), ), }; }).pipe(Effect.provide(Cloudflare.SecretsStore.ReadSecretBinding)),) {}The client returned by ReadSecret is itself an Effect that resolves
to the secret’s current value, so yield* authToken reads it —
.get() does the same thing as a callable, and .raw exposes the
underlying Cloudflare SecretsStoreSecret binding. Reads can fail
with a typed SecretError, handled here with Effect.catchTag.
Add to the stack
Section titled “Add to the stack”import * as Alchemy from "alchemy";import * as Cloudflare from "alchemy/Cloudflare";import * as Output from "alchemy/Output";import * as Effect from "effect/Effect";import * as Redacted from "effect/Redacted";import Api from "./src/api.ts";import { AuthTokenValue } from "./src/auth.ts";
export default Alchemy.Stack( "WorkerAuth", { providers: Cloudflare.providers(), state: Cloudflare.state(), }, Effect.gen(function* () { const authToken = yield* AuthTokenValue; const api = yield* Api;
return { url: api.url.as<string>(), authToken: authToken.text.pipe(Output.map(Redacted.value)), }; }),);authToken.text is an Output<Redacted<string>>. Mapping over the
Output with Redacted.value unwraps it so the stack output emits the
real token — otherwise it JSON-serializes to the literal string
"<redacted>".
Deploy and test
Section titled “Deploy and test”alchemy deploycurl https://<your-worker>.workers.dev/protected# → unauthorized (401)
curl -H "Authorization: Bearer <authToken from the deploy output>" \ https://<your-worker>.workers.dev/protected# → okAsync Workers: bind via env
Section titled “Async Workers: bind via env”Async (non-Effect) Workers don’t have an init phase to yield* a
binding into. Declare the Secret on the Worker’s env instead — the
provider maps it to a native secrets_store_secret binding, so the
runtime sees a real SecretsStoreSecret with a .get() method:
import type { SecretsStoreSecret } from "@cloudflare/workers-types";
export default class Api extends Cloudflare.Worker<Api>()( "Api", { main: import.meta.url, env: { AUTH_TOKEN: AuthToken, }, }, Effect.gen(function* () { return { fetch: Effect.gen(function* () { const env = yield* Cloudflare.Workers.WorkerEnvironment; const secret = (env as Record<string, SecretsStoreSecret>).AUTH_TOKEN; const value = yield* Effect.promise(() => secret.get()); // ... }), }; }),) {}Env vars vs Secrets Store
Section titled “Env vars vs Secrets Store”Both end up as bindings on the Worker — pick based on where the value lives and who shares it:
- Env vars (
Config.redacted) — the value comes from your environment (.env, CI secrets) at deploy time and is baked into that one Worker. Right for third-party API keys and per-app config. See Secrets & env for the step-by-step. - Secrets Store — the value lives in Cloudflare’s account-level store. One secret can be bound into many Workers, reads happen at runtime so a rotation propagates without redeploying every consumer, and the dashboard redacts it. Right for shared credentials and tokens your infrastructure owns — like the bearer token in this guide.
Where next
Section titled “Where next”- Workers — the host the secret binds into.
- Secrets and Config — how env-var bindings work under the hood.
- Secrets & env — the category anchor: Config, Random, and when to use the store.
Reference: