Skip to content

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.

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)}`);
}),
};
}),
);

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);
}),
};
}),
);

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 PORT is set, its raw value is bound. At runtime Config.Number("PORT") reads it from the binding and the combinators re-apply.
  • If PORT is not set, nothing is bound. At runtime the source is still empty and Config.withDefault(3000) produces 3000 again.

Because a default is never bound, its code runs in both phases — keep it deterministic, so both phases evaluate to the same value.

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 Runtime
Effect.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 (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:

alchemy.run.ts
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>;
src/worker.ts
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.