Skip to content

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.

Any Config yielded in the Worker’s Construction 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.

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

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 the constructor, not in fetch

Section titled “Resolve Config in the constructor, 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 runtime
Effect.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 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));
}),
};
});

Async (non-Effect) Workers have no Construction phase — declare bindings on the env prop and type the handler with InferEnv:

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

Literal values route by shape: Redacted<string>secret_text, stringplain_text, anything else → json. Output values (e.g. Alchemy.makeRandom) classify by their resolved value with the same rules, so a resolved Redacted still uploads as secret_text.

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.

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.

Related:

Reference: