Skip to content

Secrets and env vars

This guide walks through the most common env-var task: take a secret like OPENAI_API_KEY from your shell or a .env file and make it available inside a Worker as a secret_text binding — without leaking it through plans, logs, or the Worker bundle.

The same shape works for non-secret config too — swap Alchemy.Secret for Alchemy.Variable and the binding ships as plain_text / json instead.

Put the value somewhere the active ConfigProvider can find it. The CLI uses ConfigProvider.fromEnv() by default, so either an exported shell var or a .env file at the project root works:

.env
OPENAI_API_KEY=sk-proj-...

You don’t need to plumb process.env anywhere — Alchemy.Secret will read from ConfigProvider for you.

Inside the Worker’s init phase, yield* Alchemy.Secret("OPENAI_API_KEY") records the binding and returns an accessor for the runtime value.

src/worker.ts
import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
export default Cloudflare.Worker(
"Worker",
{ main: import.meta.path },
Effect.gen(function* () {
const apiKey = yield* Alchemy.Secret("OPENAI_API_KEY");
return {
fetch: Effect.gen(function* () {
return HttpServerResponse.text("Hello, world!");
}),
};
}),
);

The 1-arg form is sugar for Alchemy.Secret("OPENAI_API_KEY", Config.redacted("OPENAI_API_KEY")). At deploy time Alchemy resolves the value, hands it to the Worker provider as a Redacted<string>, and the put-worker payload declares the binding as type: "secret_text".

apiKey is an Effect<Redacted<string>> — an accessor. yield* it inside fetch to get the value, then unwrap with Redacted.value when you actually need the string (e.g. an Authorization header):

src/worker.ts
import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
import * as Redacted from "effect/Redacted";
export default Cloudflare.Worker(
"Worker",
{ main: import.meta.path },
Effect.gen(function* () {
const apiKey = yield* Alchemy.Secret("OPENAI_API_KEY");
return {
fetch: Effect.gen(function* () {
const key = yield* apiKey;
const response = yield* Effect.tryPromise(() =>
fetch("https://api.openai.com/v1/models", {
headers: { Authorization: `Bearer ${Redacted.value(key)}` },
}),
);
return HttpServerResponse.text(await response.text());
return HttpServerResponse.text("Hello, world!");
}),
};
}),
);

Redacted keeps the value out of logs and stack traces — the Bearer … string only exists inside the fetch call.

The 2-arg form replaces Config.redacted(name) with whatever you want. Useful when the env var on disk has a different name, or when the value comes from a different system entirely:

import * as Config from "effect/Config";
// Read from a differently-named env var
yield* Alchemy.Secret("OPENAI_API_KEY", Config.redacted("MY_OPENAI_KEY"));
// Pull from another resource's output
const stored = yield* Cloudflare.Secret("ApiKey", { store, value: ... });
yield* Alchemy.Secret("OPENAI_API_KEY", stored.value);
// Compute the value with an Effect
yield* Alchemy.Secret("OPENAI_API_KEY", Effect.gen(function* () {
const raw = yield* fetchKeyFromVault();
return Redacted.make(raw);
}));

The binding name on the Worker (the key the runtime reads) is always the first argument — the second argument is just where the value comes from.

For values that are fine to ship as plain text, swap to Alchemy.Variable. Strings deploy as plain_text; everything else deploys as a json binding (Cloudflare parses it back into the original JS value at runtime):

const host = yield* Alchemy.Variable("HOST", "api.openai.com");
const port = yield* Alchemy.Variable("PORT", 443);
const flags = yield* Alchemy.Variable("FLAGS", { beta: true });

Read them inside fetch the same way — yield* host gives back the resolved value (no Redacted.value unwrap needed).

You now have:

  • OPENAI_API_KEY read from Config at deploy time
  • A secret_text binding deployed onto the Worker (no plain-text copy in the bundle, plan, or logs)
  • A typed runtime accessor that returns Redacted<string>

For the bigger picture — input shapes, the deploy-vs-runtime phases, and when to reach for Cloudflare’s Secrets Store instead — see Concepts › Secrets and Variables.