Skip to content

Secret providers

Set the optional secrets field to choose where a stack’s Effect Config values come from: one provider or an ordered list. Omitting it preserves existing configuration, including any injected ConfigProvider. The CLI normally reads the process environment with .env as a fallback; an explicit --env-file takes precedence over the process environment.

import * as Doppler from "alchemy/Doppler";
export default Alchemy.Stack(
"MyApp",
{
providers: Cloudflare.providers(),
state: Cloudflare.state(),
secrets: [
Doppler.Secrets({ project: "my-app", config: "dev" }),
Alchemy.Secrets.DotEnv(),
],
},
Effect.gen(function* () {
const dbUrl = yield* Config.Redacted("DATABASE_URL");
// ...
}),
);

Later entries override earlier ones. In the example above, process environment variables override .env, which overrides Doppler.

The process environment is the implicit last entry, so a one-off DATABASE_URL=... alchemy deploy and a CI runner’s exported tokens work with any list. List Secrets.ProcessEnv() yourself to rank it elsewhere, typically first so it is only a fallback for what the secrets manager holds:

secrets: [
Alchemy.Secrets.ProcessEnv(),
Doppler.Secrets({ project: "my-app", config: "prd" }),
],

Secrets.ProcessEnv({ disabled: true }) leaves the shell out entirely. Cloud credentials such as CLOUDFLARE_API_TOKEN go through the same list, so with the shell disabled they must come from a provider. A secrets manager’s own credentials are the exception: it reads DOPPLER_TOKEN, INFISICAL_IDENTITY_ID, and CI from the process environment only, whatever the list says, so a runner’s exported variables always reach it and no file can impersonate them.

For stacks that declare secrets, one key is exempt: ALCHEMY_PROFILE. Which profile a deploy authenticates with comes from --profile or the real process environment only, so no provider can redirect the credentials every other provider uses.

Both the secrets option and every provider’s options accept a callback instead of a value. It receives the stage, the stack, and the Alchemy context, so one stack definition serves dev, pr-42, and prod from different places:

secrets: ({ stage }) =>
stage === "dev"
? Alchemy.Secrets.DotEnv()
: Doppler.Secrets({ project: "my-app", config: stage }),

The same shape narrows a single provider:

secrets: [
Alchemy.Secrets.DotEnv(({ stage }) => ({ path: [".env", `.env.${stage}`] })),
],

Secrets.DotEnv reads one file or an ordered list. The implicit default .env may be absent; a file you name must exist. Empty values are real values and override earlier files. Pass expandVariables: true to expand ${OTHER} references.

Nothing is written to process.env. Values live in Effect’s ConfigProvider, so two stacks in one process never see each other’s files.

--env-file retains its existing behavior when secrets is omitted. It is refused when the stack declares secrets; put Secrets.DotEnv({ path }) in the list instead.

With --log-level debug, dotenv, Doppler, and Infisical report the variable names they loaded, never their values. The process environment reports only a count. These messages describe loaded sources; process environment overrides and custom layers can still change the final values.

Loaded 3 secrets from dotenv (.env, .env.prod): DATABASE_URL, PORT, SENTRY_DSN
Loaded 12 secrets from Doppler (project 'app' config 'prd'): API_KEY, DATABASE_URL, ...
Loaded 87 secrets from the process environment

Any Effect Layer that provides a ConfigProvider is a valid entry. Use ConfigProvider.layerAdd with asPrimary to merge with what came before, the way the built-in providers do; a plain ConfigProvider.layer replaces everything listed before it:

secrets: [
ConfigProvider.layerAdd(ConfigProvider.fromUnknown(defaults), { asPrimary: true }),
Alchemy.Secrets.DotEnv(),
],

The Doppler and Infisical providers skip remote loading during auth-provider discovery, which builds the stack offline just to find out which clouds it uses.

  • Doppler — browser login locally, service tokens in CI.
  • Infisical — machine identities locally, platform OIDC in CI.
  • Secrets & Config — how a Config value ends up bound to a Worker or Lambda.