| 1 | --- |
| 2 | title: Secrets & Config |
| 3 | description: Use effect/Config to read env vars at Construction time and have Alchemy automatically bind them onto the deploy target. |
| 4 | --- |
| 5 | |
| 6 | Alchemy integrates with [effect/Config](https://effect.website/docs/configuration) |
| 7 | to automatically bind env vars to your Worker or Lambda environment ([Runtime](/infrastructure-as-effects/runtime)). |
| 8 | Bound values come from the env of whoever runs the deploy, so they naturally |
| 9 | vary per [stage](/environments/stages) and [profile](/environments/profiles). |
| 10 | |
| 11 | Where those values come from is the stack's business. By default |
| 12 | that is `.env` plus the shell; a stack can instead read them from |
| 13 | [Doppler](/environments/doppler) or [Infisical](/environments/infisical), |
| 14 | and layer several sources in a chosen order. |
| 15 | See [Secret providers](/environments/secret-providers). This page is |
| 16 | about what happens after a value is read. |
| 17 | |
| 18 | ## Bind a secret to your Worker |
| 19 | |
| 20 | Any `Config` value that is evaluated with `yield*` in the [Construction phase](/infrastructure-as-effects/phases) |
| 21 | of (for example) a Worker, is automatically bound to its environment |
| 22 | at deploy time. It is always bound as a secret (`secret_text` on |
| 23 | Cloudflare) regardless of which `Config` constructor you use. |
| 24 | |
| 25 | ```typescript |
| 26 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 27 | import * as Config from "effect/Config"; |
| 28 | import * as Effect from "effect/Effect"; |
| 29 | import * as Redacted from "effect/Redacted"; |
| 30 | |
| 31 | export default Cloudflare.Worker( |
| 32 | "Worker", |
| 33 | { main: import.meta.url }, |
| 34 | Effect.gen(function* () { |
| 35 | const apiKey = yield* Config.Redacted("API_KEY"); |
| 36 | // apiKey is Redacted<string> — usable here in Construction AND captured as a binding |
| 37 | |
| 38 | return { |
| 39 | fetch: Effect.gen(function* () { |
| 40 | return new Response(`Bearer ${Redacted.value(apiKey)}`); |
| 41 | }), |
| 42 | }; |
| 43 | }), |
| 44 | ); |
| 45 | ``` |
| 46 | |
| 47 | :::tip |
| 48 | See [Phases](/infrastructure-as-effects/phases) for why Construction and Runtime run separately. |
| 49 | ::: |
| 50 | |
| 51 | ## Use the value during Construction |
| 52 | |
| 53 | Unlike an [Output](/infrastructure-as-code/outputs), the value can be used immediately |
| 54 | within the Construction phase. For example, to initialize a client: |
| 55 | |
| 56 | ```typescript |
| 57 | export default Cloudflare.Worker( |
| 58 | "Worker", |
| 59 | { main: import.meta.url }, |
| 60 | Effect.gen(function* () { |
| 61 | const apiKey = yield* Config.Redacted("OPENAI_API_KEY"); |
| 62 | const client = createOpenAI(Redacted.value(apiKey)); |
| 63 | |
| 64 | return { |
| 65 | fetch: Effect.gen(function* () { |
| 66 | const reply = yield* Effect.tryPromise(() => client.chat(/* ... */)); |
| 67 | return new Response(reply); |
| 68 | }), |
| 69 | }; |
| 70 | }), |
| 71 | ); |
| 72 | ``` |
| 73 | |
| 74 | |
| 75 | ## Transformations |
| 76 | |
| 77 | Any combinator works — `withDefault`, `orElse`, `mapEffect`, and so |
| 78 | on. What gets bound is the *source* value (the raw env var), not the |
| 79 | transformed result; the combinators run again at runtime against that |
| 80 | source. |
| 81 | |
| 82 | ```typescript |
| 83 | const port = yield* Config.Number("PORT").pipe( |
| 84 | Config.withDefault(3000), |
| 85 | ); |
| 86 | ``` |
| 87 | |
| 88 | - If `PORT` is set, its raw value is bound. At runtime `Config.Number("PORT")` |
| 89 | reads it from the binding and the combinators re-apply. |
| 90 | - If `PORT` is not set, nothing is bound. At runtime the source is |
| 91 | still empty and `Config.withDefault(3000)` produces `3000` again. |
| 92 | |
| 93 | Because a default is never bound, its code runs in both phases — keep |
| 94 | it deterministic, so both phases evaluate to the same value. |
| 95 | |
| 96 | ## Footgun: don't `yield*` Config at runtime |
| 97 | |
| 98 | If you only `yield*` the `Config` in `fetch`, it will not be bound to the environment |
| 99 | because `fetch` does not run during deployment, so the engine never discovers it. |
| 100 | |
| 101 | ```typescript |
| 102 | export default Cloudflare.Worker( |
| 103 | "Worker", |
| 104 | { main: import.meta.url }, |
| 105 | Effect.gen(function* () { |
| 106 | return { |
| 107 | fetch: Effect.gen(function* () { |
| 108 | // 🚫 nothing is bound to the Worker — API_KEY won't exist at runtime |
| 109 | const apiKey = yield* Config.Redacted("API_KEY"); |
| 110 | // ... |
| 111 | }), |
| 112 | }; |
| 113 | }), |
| 114 | ); |
| 115 | ``` |
| 116 | |
| 117 | Always resolve the `Config` in the outer `Effect.gen` (Construction) — even |
| 118 | if you only need the value inside `fetch`. Capture it in a `const`, |
| 119 | and reference that `const` from the runtime body: |
| 120 | |
| 121 | ```typescript |
| 122 | // ✅ bound in Construction, used in Runtime |
| 123 | Effect.gen(function* () { |
| 124 | const apiKey = yield* Config.Redacted("API_KEY"); |
| 125 | return { |
| 126 | fetch: Effect.gen(function* () { |
| 127 | return new Response(Redacted.value(apiKey)); |
| 128 | }), |
| 129 | }; |
| 130 | }); |
| 131 | ``` |
| 132 | |
| 133 | Once a `Config` has been resolved during Construction, re-yielding the same |
| 134 | `Config` anywhere at runtime — inside `fetch`, deep in a nested |
| 135 | effect, or in a service layer — resolves it from the binding and |
| 136 | produces the same value. |
| 137 | |
| 138 | ## Async Workers — bind via `env` |
| 139 | |
| 140 | Async (non-Effect) Workers don't have a constructor `Effect.gen` to |
| 141 | `yield*` into, so you put the `Config` on the resource's `env` |
| 142 | prop instead. Alchemy resolves each `Config` at deploy time and |
| 143 | records the appropriate binding — same end result as the `yield*` |
| 144 | form, just declared statically: |
| 145 | |
| 146 | ```typescript |
| 147 | // alchemy.run.ts |
| 148 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 149 | import * as Config from "effect/Config"; |
| 150 | |
| 151 | export const Worker = Cloudflare.Worker("Worker", { |
| 152 | main: "./src/worker.ts", |
| 153 | env: { |
| 154 | API_KEY: Config.Redacted("API_KEY"), |
| 155 | HOST: Config.String("HOST"), |
| 156 | Bucket, // resource references work the same |
| 157 | }, |
| 158 | }); |
| 159 | |
| 160 | export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>; |
| 161 | ``` |
| 162 | |
| 163 | ```typescript |
| 164 | // src/worker.ts |
| 165 | import type { WorkerEnv } from "../alchemy.run.ts"; |
| 166 | |
| 167 | export default { |
| 168 | async fetch(request: Request, env: WorkerEnv) { |
| 169 | return new Response(`Bearer ${env.API_KEY}`); |
| 170 | }, |
| 171 | }; |
| 172 | ``` |
| 173 | |
| 174 | `env.API_KEY` is the resolved string at runtime — Cloudflare hands |
| 175 | the `secret_text` value to the async handler directly, no |
| 176 | `Redacted.value` unwrap. The always-binds-as-secret rule and |
| 177 | transformation semantics from the previous sections still apply. |
| 178 | |
| 179 | For the step-by-step "wire up `OPENAI_API_KEY` from `.env`" walk, |
| 180 | see [Secrets & env on Cloudflare](/cloudflare/security/secrets-env) or |
| 181 | [on AWS](/aws/security/secrets-env). |
| 182 | |
| 183 | ## Where next |
| 184 | |
| 185 | - [Secret providers](/environments/secret-providers) — where `Config` values come from and in what order. |
| 186 | - [Local development](/environments/local-development) — `alchemy dev`: local code, real cloud resources. |
| 187 | - [Secrets & env on Cloudflare](/cloudflare/security/secrets-env) — wire a real key from `.env` into a Worker. |
| 188 | - [Secrets & env on AWS](/aws/security/secrets-env) — the same walk for Lambda. |