| 1 | --- |
| 2 | title: Secrets & env |
| 3 | description: Bind env vars and secrets to Workers with effect/Config, generate stable tokens with Alchemy.Random, and graduate to Secrets Store when secrets are shared across Workers. |
| 4 | --- |
| 5 | |
| 6 | Three ways to get a secret into a Worker, by where the value lives. |
| 7 | A value from your `.env` that belongs to one Worker — |
| 8 | `Config.Redacted`. A token your infrastructure mints — |
| 9 | `Alchemy.Random`. A secret shared across Workers that must rotate |
| 10 | without a redeploy — [Secrets Store](#graduate-to-secrets-store). |
| 11 | |
| 12 | ## Bind a secret from .env |
| 13 | |
| 14 | Any `Config` yielded in the Worker's Construction phase is read from your |
| 15 | environment at deploy time, bound to the Worker, and resolved from |
| 16 | that binding at runtime: |
| 17 | |
| 18 | ```typescript |
| 19 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 20 | import * as Config from "effect/Config"; |
| 21 | import * as Effect from "effect/Effect"; |
| 22 | import * as Redacted from "effect/Redacted"; |
| 23 | |
| 24 | export default Cloudflare.Worker( |
| 25 | "Worker", |
| 26 | { main: import.meta.url }, |
| 27 | Effect.gen(function* () { |
| 28 | const apiKey = yield* Config.Redacted("API_KEY"); |
| 29 | |
| 30 | return { |
| 31 | fetch: Effect.gen(function* () { |
| 32 | return new Response(`Bearer ${Redacted.value(apiKey)}`); |
| 33 | }), |
| 34 | }; |
| 35 | }), |
| 36 | ); |
| 37 | ``` |
| 38 | |
| 39 | `Config` values bind as `secret_text` regardless of the constructor |
| 40 | used. |
| 41 | |
| 42 | ## Unwrap at the call site |
| 43 | |
| 44 | The value is a `Redacted<string>` — unwrap with `Redacted.value` |
| 45 | only where the raw string is needed: |
| 46 | |
| 47 | ```typescript |
| 48 | const response = yield* Effect.tryPromise(() => |
| 49 | fetch("https://api.openai.com/v1/models", { |
| 50 | headers: { Authorization: `Bearer ${Redacted.value(apiKey)}` }, |
| 51 | }), |
| 52 | ); |
| 53 | ``` |
| 54 | |
| 55 | :::caution |
| 56 | Coercing a `Redacted` into a template literal yields the literal |
| 57 | string `"<redacted>"` — `` `Bearer ${apiKey}` `` sends |
| 58 | `Bearer <redacted>`. |
| 59 | ::: |
| 60 | |
| 61 | ## Plain vars and defaults |
| 62 | |
| 63 | Non-secret config uses `Config.String` / `Config.Number`, and any |
| 64 | combinator works: |
| 65 | |
| 66 | ```typescript |
| 67 | const port = yield* Config.Number("PORT").pipe( |
| 68 | Config.withDefault(3000), |
| 69 | ); |
| 70 | ``` |
| 71 | |
| 72 | The raw source value is bound; combinators re-run at runtime against |
| 73 | it, and a default is never bound. See |
| 74 | [Secrets and Config](/environments/secrets) for the full semantics. |
| 75 | |
| 76 | ## Resolve Config in the constructor, not in fetch |
| 77 | |
| 78 | `fetch` never runs at deploy time, so a `Config` yielded only there |
| 79 | is never discovered or bound: |
| 80 | |
| 81 | ```typescript |
| 82 | // 🚫 nothing is bound — API_KEY won't exist at runtime |
| 83 | Effect.gen(function* () { |
| 84 | return { |
| 85 | fetch: Effect.gen(function* () { |
| 86 | const apiKey = yield* Config.Redacted("API_KEY"); |
| 87 | // ... |
| 88 | }), |
| 89 | }; |
| 90 | }); |
| 91 | ``` |
| 92 | |
| 93 | Resolve it in the outer `Effect.gen` and reference the `const` from |
| 94 | the handler: |
| 95 | |
| 96 | ```typescript |
| 97 | // ✅ bound in Construction, used in Runtime |
| 98 | Effect.gen(function* () { |
| 99 | const apiKey = yield* Config.Redacted("API_KEY"); |
| 100 | return { |
| 101 | fetch: Effect.gen(function* () { |
| 102 | return new Response(Redacted.value(apiKey)); |
| 103 | }), |
| 104 | }; |
| 105 | }); |
| 106 | ``` |
| 107 | |
| 108 | ## Async Workers: the env prop |
| 109 | |
| 110 | Async (non-Effect) Workers have no Construction phase — declare bindings on |
| 111 | the `env` prop and type the handler with `InferEnv`: |
| 112 | |
| 113 | ```typescript |
| 114 | // alchemy.run.ts |
| 115 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 116 | import * as Config from "effect/Config"; |
| 117 | |
| 118 | export const Worker = Cloudflare.Worker("Worker", { |
| 119 | main: "./src/worker.ts", |
| 120 | env: { |
| 121 | API_KEY: Config.Redacted("API_KEY"), |
| 122 | HOST: Config.String("HOST"), |
| 123 | Bucket, // resource references work the same |
| 124 | }, |
| 125 | }); |
| 126 | |
| 127 | export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>; |
| 128 | ``` |
| 129 | |
| 130 | ```typescript |
| 131 | // src/worker.ts |
| 132 | import type { WorkerEnv } from "../alchemy.run.ts"; |
| 133 | |
| 134 | export default { |
| 135 | async fetch(request: Request, env: WorkerEnv) { |
| 136 | return new Response(`Bearer ${env.API_KEY}`); |
| 137 | }, |
| 138 | }; |
| 139 | ``` |
| 140 | |
| 141 | Literal values route by shape: `Redacted<string>` → `secret_text`, |
| 142 | `string` → `plain_text`, anything else → `json`. `Output` values |
| 143 | (e.g. `Alchemy.makeRandom`) classify by their resolved value with the |
| 144 | same rules, so a resolved `Redacted` still uploads as `secret_text`. |
| 145 | |
| 146 | ## Generate tokens with Alchemy.Random |
| 147 | |
| 148 | For tokens your infrastructure owns — webhook secrets, bearer |
| 149 | tokens — mint once with `Random` and it persists in state, so every |
| 150 | deploy keeps the same value: |
| 151 | |
| 152 | ```typescript |
| 153 | import { Random } from "alchemy"; |
| 154 | |
| 155 | export const AuthTokenValue = Random("AuthTokenValue"); |
| 156 | ``` |
| 157 | |
| 158 | 32 hex-encoded bytes by default (pass `bytes` to change); `.text` is |
| 159 | a `Redacted<string>`, so it never leaks into logs. |
| 160 | |
| 161 | ## Graduate to Secrets Store |
| 162 | |
| 163 | Env vars are baked into one Worker at deploy time. Secrets Store is |
| 164 | account-level: one secret bound into many Workers, read live at |
| 165 | runtime so a rotation propagates without redeploying consumers, and |
| 166 | redacted in the dashboard. |
| 167 | |
| 168 | ```typescript |
| 169 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 170 | import * as Effect from "effect/Effect"; |
| 171 | import { AuthTokenValue } from "./auth.ts"; |
| 172 | |
| 173 | export const Store = Cloudflare.SecretsStore.Store("AuthSecrets"); |
| 174 | |
| 175 | export const AuthToken = Effect.gen(function* () { |
| 176 | const store = yield* Store; |
| 177 | const value = yield* AuthTokenValue; |
| 178 | return yield* Cloudflare.SecretsStore.Secret("AuthToken", { |
| 179 | store, |
| 180 | value: value.text, |
| 181 | }); |
| 182 | }); |
| 183 | ``` |
| 184 | |
| 185 | ```typescript |
| 186 | export default Cloudflare.Worker( |
| 187 | "Api", |
| 188 | { main: import.meta.url }, |
| 189 | Effect.gen(function* () { |
| 190 | const authToken = yield* Cloudflare.SecretsStore.ReadSecret(AuthToken); |
| 191 | |
| 192 | return { |
| 193 | fetch: Effect.gen(function* () { |
| 194 | const token = yield* authToken; // Redacted<string>, read live |
| 195 | // ... |
| 196 | }), |
| 197 | }; |
| 198 | }).pipe(Effect.provide(Cloudflare.SecretsStore.ReadSecretBinding)), |
| 199 | ); |
| 200 | ``` |
| 201 | |
| 202 | Reads fail with a typed `SecretError` — handle it with |
| 203 | `Effect.catchTag`. For the full bearer-token walkthrough — store |
| 204 | adoption semantics, stack outputs, deploy and curl — see the |
| 205 | [Secrets Store guide](/cloudflare/security/secrets-store). |
| 206 | |
| 207 | ## Where next |
| 208 | |
| 209 | Related: |
| 210 | |
| 211 | - [Secrets Store & auth tokens](/cloudflare/security/secrets-store) — |
| 212 | full bearer-token walkthrough. |
| 213 | - [Turnstile](/cloudflare/security/turnstile) — a real third-party |
| 214 | secret key in practice. |
| 215 | - [Secrets and Config](/environments/secrets) — binding semantics under |
| 216 | the hood. |
| 217 | - [Workers](/cloudflare/compute/workers) — the host every binding lands on. |
| 218 | - [Secrets & env on AWS](/aws/security/secrets-env) — the same spine on AWS. |
| 219 | |
| 220 | Reference: |
| 221 | |
| 222 | - [Store API reference](/providers/cloudflare/secretsstore/store) |
| 223 | - [Secret API reference](/providers/cloudflare/secretsstore/secret) |
| 224 | - [ReadSecret API reference](/providers/cloudflare/secretsstore/readsecret) |