Secrets & env
Three ways to get a secret into a Worker, by where the value lives.
A value from your .env that belongs to one Worker —
Config.redacted. A token your infrastructure mints —
Alchemy.Random. A secret shared across Workers that must rotate
without a redeploy — Secrets Store.
Bind a secret from .env
Section titled “Bind a secret from .env”Any Config yielded in the Worker’s init phase is read from your
environment at deploy time, bound to the Worker, and resolved from
that binding at runtime:
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");
return { fetch: Effect.gen(function* () { return new Response(`Bearer ${Redacted.value(apiKey)}`); }), }; }),);Config values bind as secret_text regardless of the constructor
used.
Unwrap at the call site
Section titled “Unwrap at the call site”The value is a Redacted<string> — unwrap with Redacted.value
only where the raw string is needed:
const response = yield* Effect.tryPromise(() => fetch("https://api.openai.com/v1/models", { headers: { Authorization: `Bearer ${Redacted.value(apiKey)}` }, }),);Plain vars and defaults
Section titled “Plain vars and defaults”Non-secret config uses Config.string / Config.number, and any
combinator works:
const port = yield* Config.number("PORT").pipe( Config.withDefault(3000),);The raw source value is bound; combinators re-run at runtime against it, and a default is never bound. See Secrets and Config for the full semantics.
Resolve Config in init, not in fetch
Section titled “Resolve Config in init, not in fetch”fetch never runs at deploy time, so a Config yielded only there
is never discovered or bound:
// 🚫 nothing is bound — API_KEY won't exist at runtimeEffect.gen(function* () { return { fetch: Effect.gen(function* () { const apiKey = yield* Config.redacted("API_KEY"); // ... }), };});Resolve it in the outer Effect.gen and reference the const from
the handler:
// ✅ bound in init, used in runtimeEffect.gen(function* () { const apiKey = yield* Config.redacted("API_KEY"); return { fetch: Effect.gen(function* () { return new Response(Redacted.value(apiKey)); }), };});Async Workers: the env prop
Section titled “Async Workers: the env prop”Async (non-Effect) Workers have no init phase — declare bindings on
the env prop and type the handler with InferEnv:
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}`); },};Literal values route by shape: Redacted<string> → secret_text,
string → plain_text, anything else → json.
Generate tokens with Alchemy.Random
Section titled “Generate tokens with Alchemy.Random”For tokens your infrastructure owns — webhook secrets, bearer
tokens — mint once with Random and it persists in state, so every
deploy keeps the same value:
import { Random } from "alchemy";
export const AuthTokenValue = Random("AuthTokenValue");32 hex-encoded bytes by default (pass bytes to change); .text is
a Redacted<string>, so it never leaks into logs.
Graduate to Secrets Store
Section titled “Graduate to Secrets Store”Env vars are baked into one Worker at deploy time. Secrets Store is account-level: one secret bound into many Workers, read live at runtime so a rotation propagates without redeploying consumers, and redacted in the dashboard.
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import { AuthTokenValue } from "./auth.ts";
export const Store = Cloudflare.SecretsStore.Store("AuthSecrets");
export const AuthToken = Effect.gen(function* () { const store = yield* Store; const value = yield* AuthTokenValue; return yield* Cloudflare.SecretsStore.Secret("AuthToken", { store, value: value.text, });});export default Cloudflare.Worker( "Api", { main: import.meta.url }, Effect.gen(function* () { const authToken = yield* Cloudflare.SecretsStore.ReadSecret(AuthToken);
return { fetch: Effect.gen(function* () { const token = yield* authToken; // Redacted<string>, read live // ... }), }; }).pipe(Effect.provide(Cloudflare.SecretsStore.ReadSecretBinding)),);Reads fail with a typed SecretError — handle it with
Effect.catchTag. For the full bearer-token walkthrough — store
adoption semantics, stack outputs, deploy and curl — see the
Secrets Store guide.
Where next
Section titled “Where next”Related:
- Secrets Store & auth tokens — full bearer-token walkthrough.
- Turnstile — a real third-party secret key in practice.
- Secrets and Config — binding semantics under the hood.
- Workers — the host every binding lands on.
- Secrets & env on AWS — the same spine on AWS.
Reference: