Skip to content

Secrets and Variables

A secret is any sensitive value your Worker or Function needs at runtime — an API key, a database password, a signing key, an OAuth client secret. You don’t want it in source code, you don’t want it in plain-text env vars on disk, and you don’t want it echoed in logs.

Alchemy.Secret binds a secret value directly to the active deploy target (a Cloudflare Worker, a Lambda function, …) through that platform’s secure binding — Cloudflare’s secret_text, Lambda’s encrypted environment variable, etc. — and hands you back an accessor that reads it at runtime as a Redacted<string>.

Alchemy.Variable is the same shape for non-sensitive config (ports, log levels, feature flags). The value rides as plain text (or JSON for non-strings) and the accessor preserves the original type.

yield*-ing Alchemy.Secret in the Worker’s Init phase registers the binding and returns an accessor. yield*-ing the accessor inside fetch (the Exec phase) resolves the value:

export default Cloudflare.Worker(
"Worker",
{ main: import.meta.path },
Effect.gen(function* () {
const apiKey = yield* Alchemy.Secret("API_KEY");
return {
fetch: Effect.gen(function* () {
const value = yield* apiKey;
// Redacted<string> — Redacted.value(value) to unwrap
}),
};
}),
);

See Phases for why Init and Exec run separately.

Alchemy.Secret("API_KEY") with no second argument is sugar for Alchemy.Secret("API_KEY", Config.redacted("API_KEY")). The active ConfigProvider resolves the value at planning time — by default that reads process.env.API_KEY, which Alchemy populates from your .env file.

So this:

const apiKey = yield* Alchemy.Secret("API_KEY");

is all you need if API_KEY=… is in .env.

To pull from a differently-named env var, pass an explicit Config:

const apiKey = yield* Alchemy.Secret("API_KEY", Config.redacted("OPENAI_KEY"));

The second argument accepts a literal, a Redacted<string>, an Effect, a Config, or an Output from another resource.

A common pattern: mint a stable random secret with Alchemy.Random and bind it as a secret on the Worker — no .env entry, no manual rotation, the value is generated once and persisted in state:

export default Cloudflare.Worker(
"Worker",
{ main: import.meta.path },
Effect.gen(function* () {
const random = yield* Alchemy.Random("SESSION_SECRET");
const sessionSecret = yield* Alchemy.Secret("SESSION_SECRET", random.text);
return {
fetch: Effect.gen(function* () {
const value = yield* sessionSecret; // Redacted<string>
}),
};
}),
);

Any Output<Redacted<string>> works the same way — e.g. a database password emitted by another resource can be piped straight into the consuming Worker.

Alchemy.Variable is the non-redacted variant. Strings ride as plain_text, anything else as JSON, and the accessor returns the original type:

export default Cloudflare.Worker(
"Worker",
{ main: import.meta.path },
Effect.gen(function* () {
const port = yield* Alchemy.Variable("PORT", 3000);
const flags = yield* Alchemy.Variable("FLAGS", { beta: true });
return {
fetch: Effect.gen(function* () {
const p = yield* port; // number — 3000
const f = yield* flags; // { beta: true }
}),
};
}),
);

Alchemy.Variable(name) is sugar for Alchemy.Variable(name, Config.string(name)).

Both helpers accept the same shapes; Secret coerces every resolved value to Redacted<string>, Variable keeps the value’s original type:

Alchemy.Secret("API_KEY"); // Config.redacted("API_KEY")
Alchemy.Secret("API_KEY", "sk-123"); // string literal
Alchemy.Secret("API_KEY", Redacted.make("sk-123")); // already redacted
Alchemy.Secret("API_KEY", Effect.succeed("sk-123")); // Effect<string | Redacted>
Alchemy.Secret("API_KEY", Config.string("OPENAI_KEY")); // Config<string | Redacted>
Alchemy.Variable("HOST"); // Config.string("HOST")
Alchemy.Variable("HOST", "localhost"); // string literal
Alchemy.Variable("PORT", 3000); // any JSON value
Alchemy.Variable("FLAGS", { beta: true }); // nested object
Alchemy.Variable("HOST", Effect.succeed("localhost")); // Effect<T>
Alchemy.Variable("LOG_LEVEL", Config.string("LEVEL")); // Config<T>

If you already have a raw string or Redacted<string> and don’t need an accessor, drop it into env directly — providers route by value shape (Redacted<string> → secure binding, string → plain, anything else → JSON). Alchemy.Secret / Alchemy.Variable earn their keep when you want .env resolution or a typed runtime accessor.

Account-wide secrets with Cloudflare.Secret

Section titled “Account-wide secrets with Cloudflare.Secret”

Cloudflare has an account-level Secrets Store. A Cloudflare.Secret lives there and can be referenced by any Worker in the account:

const store = yield* Cloudflare.SecretsStore("Store");
const apiKey = yield* Cloudflare.Secret("ApiKey", {
store,
value: Redacted.make("sk-123"),
});

For the step-by-step “wire up OPENAI_API_KEY from .env” walk, see Guides › Secrets and env vars.