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.run/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, so they’re hidden in the dashboard and in wrangler. For how Config reaches a runtime on any cloud, with defaults and Redacted values, see Secrets and Config.

.env is only the default. A Stack’s secrets option reads the same Config values from a secrets manager instead, like Doppler or Infisical, so production keys never sit in a file on anyone’s machine. Pick the source per stage:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Cloudflare from "@alchemy.run/cloudflare";
import * as Doppler from "@alchemy.run/doppler";
import * as Effect from "effect/Effect";
import Api from "./src/api.ts";
export default Alchemy.Stack(
"MyApp",
{
providers: Cloudflare.providers(),
state: Cloudflare.state(),
secrets: ({ stage }) =>
stage === "prod"
? Doppler.Secrets({ project: "my-app", config: "prd" })
: Alchemy.Secrets.DotEnv(),
},
Effect.gen(function* () {
const api = yield* Api;
return { url: api.url };
}),
);

Your code doesn’t change: Config.Redacted("API_KEY") reads from whichever source the stage uses. See Secret providers for Doppler, Infisical, ordering several sources, and writing your own.

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.run/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, string → plain_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.run/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: