1---
2title: Secrets Store & auth tokens
3description: Store secrets in Cloudflare's account-level Secrets Store, generate stable tokens with Alchemy.Random, and read them in a Worker through the ReadSecret binding.
4sidebar:
5 order: 18
6---
7
8Cloudflare's Secrets Store is an account-level container for secrets.
9Unlike a plain env var, a stored secret is shared across Workers,
10redacted in the dashboard, and read live at runtime — bound Workers
11see the current value without a redeploy.
12
13In this guide you'll build a bearer-token-protected Worker: a
14`Store`, a token minted with `Alchemy.Random`, a `Secret` holding it,
15and a `ReadSecret` binding that checks it on every request.
16
17## Create the store
18
19```typescript
20// src/auth.ts
21import * as Cloudflare from "alchemy/Cloudflare";
22
23export const Store = Cloudflare.SecretsStore.Store("AuthSecrets");
24```
25
26Cloudflare allows **one Secrets Store per account**, so the provider
27adopts the existing store if one exists (creating it only on a truly
28fresh account) and never deletes it on `alchemy destroy` — it's
29account-level infrastructure that outlives any single stack.
30
31## Generate a stable token
32
33```typescript
34// src/auth.ts
35import { Random } from "alchemy";
36
37export const AuthTokenValue = Random("AuthTokenValue");
38```
39
40`Random` mints a random value once (32 bytes, hex-encoded — pass
41`bytes` to change the length) and persists it in state, so the token
42stays the same on every subsequent deploy. Its `text` attribute is a
43`Redacted<string>`, so it never leaks into logs.
44
45## Put it in the store
46
47```typescript
48// src/auth.ts
49import * as Effect from "effect/Effect";
50
51export const AuthToken = Effect.gen(function* () {
52 const store = yield* Store;
53 const value = yield* AuthTokenValue;
54 return yield* Cloudflare.SecretsStore.Secret("AuthToken", {
55 store,
56 value: value.text,
57 });
58});
59```
60
61`Secret` writes the value into the store under the resource's logical
62ID (pass `name` to override). The value is any `Redacted<string>` —
63`Redacted.make(process.env.API_KEY!)` works just as well as a
64`Random` output. Changing the value updates the secret in place;
65renaming it or moving it to another store replaces it.
66
67## Read it in a Worker
68
69Bind the secret in the Worker's Construction phase with `ReadSecret`, then
70check the `Authorization` header in `fetch`:
71
72```typescript
73// src/api.ts
74import * as Cloudflare from "alchemy/Cloudflare";
75import * as Effect from "effect/Effect";
76import * as Redacted from "effect/Redacted";
77import { HttpServerRequest } from "effect/http/HttpServerRequest";
78import * as HttpServerResponse from "effect/http/HttpServerResponse";
79import { AuthToken } from "./auth.ts";
80
81export default class Api extends Cloudflare.Worker<Api>()(
82 "Api",
83 {
84 main: import.meta.url,
85 },
86 Effect.gen(function* () {
87 const authToken = yield* Cloudflare.SecretsStore.ReadSecret(AuthToken);
88
89 return {
90 fetch: Effect.gen(function* () {
91 const request = yield* HttpServerRequest;
92
93 if (request.url.startsWith("/protected")) {
94 const authHeader = request.headers.authorization;
95 const expected = yield* authToken;
96 if (
97 !authHeader ||
98 authHeader !== `Bearer ${Redacted.value(expected)}`
99 ) {
100 return HttpServerResponse.text("unauthorized", { status: 401 });
101 }
102 return HttpServerResponse.text("ok");
103 }
104
105 return HttpServerResponse.text("public");
106 }).pipe(
107 Effect.catchTag("SecretError", (err) =>
108 Effect.succeed(
109 HttpServerResponse.text(`failed to read secret: ${err.message}`, {
110 status: 500,
111 }),
112 ),
113 ),
114 ),
115 };
116 }).pipe(Effect.provide(Cloudflare.SecretsStore.ReadSecretBinding)),
117) {}
118```
119
120The client returned by `ReadSecret` is itself an Effect that resolves
121to the secret's current value, so `yield* authToken` reads it —
122`.get()` does the same thing as a callable, and `.raw` exposes the
123underlying Cloudflare `SecretsStoreSecret` binding. Reads can fail
124with a typed `SecretError`, handled here with `Effect.catchTag`.
125
126:::caution
127The value is a `Redacted<string>` — you MUST unwrap it with
128`Redacted.value(expected)` before comparing. Coercing a `Redacted`
129into a template literal yields the literal string `"<redacted>"`, so
130`` `Bearer ${expected}` `` would compare against `"Bearer <redacted>"`
131and silently reject every real token.
132:::
133
134## Add to the stack
135
136```typescript
137// alchemy.run.ts
138import * as Alchemy from "alchemy";
139import * as Cloudflare from "alchemy/Cloudflare";
140import * as Output from "alchemy/Output";
141import * as Effect from "effect/Effect";
142import * as Redacted from "effect/Redacted";
143import Api from "./src/api.ts";
144import { AuthTokenValue } from "./src/auth.ts";
145
146export default Alchemy.Stack(
147 "WorkerAuth",
148 {
149 providers: Cloudflare.providers(),
150 state: Cloudflare.state(),
151 },
152 Effect.gen(function* () {
153 const authToken = yield* AuthTokenValue;
154 const api = yield* Api;
155
156 return {
157 url: api.url.as<string>(),
158 authToken: authToken.text.pipe(Output.map(Redacted.value)),
159 };
160 }),
161);
162```
163
164`authToken.text` is an `Output<Redacted<string>>`. Mapping over the
165Output with `Redacted.value` unwraps it so the stack output emits the
166real token — otherwise it JSON-serializes to the literal string
167`"<redacted>"`.
168
169## Deploy and test
170
171```sh
172alchemy deploy
173```
174
175```sh
176curl https://<your-worker>.workers.dev/protected
177# → unauthorized (401)
178
179curl -H "Authorization: Bearer <authToken from the deploy output>" \
180 https://<your-worker>.workers.dev/protected
181# → ok
182```
183
184## Async Workers: bind via `env`
185
186Async (non-Effect) Workers don't have a Construction phase to `yield*` a
187binding into. Declare the Secret on the Worker's `env` instead — the
188provider maps it to a native `secrets_store_secret` binding, so the
189runtime sees a real `SecretsStoreSecret` with a `.get()` method:
190
191```typescript
192import type { SecretsStoreSecret } from "@cloudflare/workers-types";
193
194export default class Api extends Cloudflare.Worker<Api>()(
195 "Api",
196 {
197 main: import.meta.url,
198 env: {
199 AUTH_TOKEN: AuthToken,
200 },
201 },
202 Effect.gen(function* () {
203 return {
204 fetch: Effect.gen(function* () {
205 const env = yield* Cloudflare.Workers.WorkerEnvironment;
206 const secret = (env as Record<string, SecretsStoreSecret>).AUTH_TOKEN;
207 const value = yield* Effect.promise(() => secret.get());
208 // ...
209 }),
210 };
211 }),
212) {}
213```
214
215## Env vars vs Secrets Store
216
217Both end up as bindings on the Worker — pick based on where the value
218lives and who shares it:
219
220- **Env vars** ([`Config.Redacted`](/environments/secrets)) — the value
221 comes from *your* environment (`.env`, CI secrets) at deploy time
222 and is baked into that one Worker. Right for third-party API keys
223 and per-app config. See [Secrets & env](/cloudflare/security/secrets-env)
224 for the step-by-step.
225- **Secrets Store** — the value lives in *Cloudflare's* account-level
226 store. One secret can be bound into many Workers, reads happen at
227 runtime so a rotation propagates without redeploying every
228 consumer, and the dashboard redacts it. Right for shared
229 credentials and tokens your infrastructure owns — like the bearer
230 token in this guide.
231
232## Where next
233
234- [Workers](/cloudflare/compute/workers) — the host the secret binds into.
235- [Secrets and Config](/environments/secrets) — how env-var bindings
236 work under the hood.
237- [Secrets & env](/cloudflare/security/secrets-env) — the category anchor:
238 Config, Random, and when to use the store.
239
240Reference:
241
242- [Store API reference](/providers/cloudflare/secretsstore/store)
243- [Secret API reference](/providers/cloudflare/secretsstore/secret)
244- [ReadSecret API reference](/providers/cloudflare/secretsstore/readsecret)