Secrets & Config
Alchemy integrates with effect/Config to automatically bind env vars to your Worker or Lambda environment (Runtime). Bound values come from the env of whoever runs the deploy, so they naturally vary per stage and profile.
Where those values come from is the stack’s business. By default
that is .env plus the shell; a stack can instead read them from
Doppler or Infisical,
and layer several sources in a chosen order.
See Secret providers. This page is
about what happens after a value is read.
Bind a secret to your Worker
Section titled “Bind a secret to your Worker”Any Config value that is evaluated with yield* in the Construction phase
of (for example) a Worker, is automatically bound to its environment
at deploy time. It is always bound as a secret (secret_text on
Cloudflare) regardless of which Config constructor you use.
import * as Cloudflare from "alchemy/Cloudflare";import * as Config from "effect/Config";import * as Effect from "effect/Effect";import * as Redacted from "effect/Redacted";
export default Cloudflare.Worker( "Worker", { main: import.meta.url }, Effect.gen(function* () { const apiKey = yield* Config.Redacted("API_KEY"); // apiKey is Redacted<string> — usable here in Construction AND captured as a binding
return { fetch: Effect.gen(function* () { return new Response(`Bearer ${Redacted.value(apiKey)}`); }), }; }),);Use the value during Construction
Section titled “Use the value during Construction”Unlike an Output, the value can be used immediately within the Construction phase. For example, to initialize a client:
export default Cloudflare.Worker( "Worker", { main: import.meta.url }, Effect.gen(function* () { const apiKey = yield* Config.Redacted("OPENAI_API_KEY"); const client = createOpenAI(Redacted.value(apiKey));
return { fetch: Effect.gen(function* () { const reply = yield* Effect.tryPromise(() => client.chat(/* ... */)); return new Response(reply); }), }; }),);Transformations
Section titled “Transformations”Any combinator works — withDefault, orElse, mapEffect, and so
on. What gets bound is the source value (the raw env var), not the
transformed result; the combinators run again at runtime against that
source.
const port = yield* Config.Number("PORT").pipe( Config.withDefault(3000),);- If
PORTis set, its raw value is bound. At runtimeConfig.Number("PORT")reads it from the binding and the combinators re-apply. - If
PORTis not set, nothing is bound. At runtime the source is still empty andConfig.withDefault(3000)produces3000again.
Because a default is never bound, its code runs in both phases — keep it deterministic, so both phases evaluate to the same value.
Footgun: don’t yield* Config at runtime
Section titled “Footgun: don’t yield* Config at runtime”If you only yield* the Config in fetch, it will not be bound to the environment
because fetch does not run during deployment, so the engine never discovers it.
export default Cloudflare.Worker( "Worker", { main: import.meta.url }, Effect.gen(function* () { return { fetch: Effect.gen(function* () { // 🚫 nothing is bound to the Worker — API_KEY won't exist at runtime const apiKey = yield* Config.Redacted("API_KEY"); // ... }), }; }),);Always resolve the Config in the outer Effect.gen (Construction) — even
if you only need the value inside fetch. Capture it in a const,
and reference that const from the runtime body:
// ✅ bound in Construction, used in RuntimeEffect.gen(function* () { const apiKey = yield* Config.Redacted("API_KEY"); return { fetch: Effect.gen(function* () { return new Response(Redacted.value(apiKey)); }), };});Once a Config has been resolved during Construction, re-yielding the same
Config anywhere at runtime — inside fetch, deep in a nested
effect, or in a service layer — resolves it from the binding and
produces the same value.
Async Workers — bind via env
Section titled “Async Workers — bind via env”Async (non-Effect) Workers don’t have a constructor Effect.gen to
yield* into, so you put the Config on the resource’s env
prop instead. Alchemy resolves each Config at deploy time and
records the appropriate binding — same end result as the yield*
form, just declared statically:
import * as Cloudflare from "alchemy/Cloudflare";import * as Config from "effect/Config";
export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", env: { API_KEY: Config.Redacted("API_KEY"), HOST: Config.String("HOST"), Bucket, // resource references work the same },});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;import type { WorkerEnv } from "../alchemy.run.ts";
export default { async fetch(request: Request, env: WorkerEnv) { return new Response(`Bearer ${env.API_KEY}`); },};env.API_KEY is the resolved string at runtime — Cloudflare hands
the secret_text value to the async handler directly, no
Redacted.value unwrap. The always-binds-as-secret rule and
transformation semantics from the previous sections still apply.
For the step-by-step “wire up OPENAI_API_KEY from .env” walk,
see Secrets & env on Cloudflare or
on AWS.
Where next
Section titled “Where next”- Secret providers — where
Configvalues come from and in what order. - Local development —
alchemy dev: local code, real cloud resources. - Secrets & env on Cloudflare — wire a real key from
.envinto a Worker. - Secrets & env on AWS — the same walk for Lambda.