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 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.

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.

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 init, 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 init 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.

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: