1---
2title: Secret providers
3description: Configure a stack with dotenv files, environment variables, Doppler, or Infisical.
4---
5
6Set the optional `secrets` field to choose where a stack's Effect `Config`
7values come from: one provider or an ordered list. Omitting it preserves
8existing configuration, including any injected `ConfigProvider`. The CLI
9normally reads the process environment with `.env` as a fallback; an
10explicit `--env-file` takes precedence over the process environment.
11
12```typescript
13import * as Doppler from "alchemy/Doppler";
14
15export default Alchemy.Stack(
16 "MyApp",
17 {
18 providers: Cloudflare.providers(),
19 state: Cloudflare.state(),
20 secrets: [
21 Doppler.Secrets({ project: "my-app", config: "dev" }),
22 Alchemy.Secrets.DotEnv(),
23 ],
24 },
25 Effect.gen(function* () {
26 const dbUrl = yield* Config.Redacted("DATABASE_URL");
27 // ...
28 }),
29);
30```
31
32## Precedence
33
34Later entries override earlier ones. In the example above, process
35environment variables override `.env`, which overrides Doppler.
36
37The process environment is the implicit last entry, so a one-off
38`DATABASE_URL=... alchemy deploy` and a CI runner's exported tokens
39work with any list. List `Secrets.ProcessEnv()` yourself to rank it
40elsewhere, typically first so it is only a fallback for what the
41secrets manager holds:
42
43```typescript
44secrets: [
45 Alchemy.Secrets.ProcessEnv(),
46 Doppler.Secrets({ project: "my-app", config: "prd" }),
47],
48```
49
50`Secrets.ProcessEnv({ disabled: true })` leaves the shell out
51entirely. Cloud credentials such as `CLOUDFLARE_API_TOKEN` go through
52the same list, so with the shell disabled they must come from a
53provider. A secrets manager's own credentials are the exception: it
54reads `DOPPLER_TOKEN`, `INFISICAL_IDENTITY_ID`, and `CI` from the
55process environment only, whatever the list says, so a runner's
56exported variables always reach it and no file can impersonate them.
57
58For stacks that declare `secrets`, one key is exempt: `ALCHEMY_PROFILE`. Which
59[profile](/environments/profiles) a deploy authenticates with comes
60from `--profile` or the real process environment only, so no provider
61can redirect the credentials every other provider uses.
62
63## Per stage
64
65Both the `secrets` option and every provider's options accept a
66callback instead of a value. It receives the
67[stage](/environments/stages), the stack, and the Alchemy context, so
68one stack definition serves `dev`, `pr-42`, and `prod` from different
69places:
70
71```typescript
72secrets: ({ stage }) =>
73 stage === "dev"
74 ? Alchemy.Secrets.DotEnv()
75 : Doppler.Secrets({ project: "my-app", config: stage }),
76```
77
78The same shape narrows a single provider:
79
80```typescript
81secrets: [
82 Alchemy.Secrets.DotEnv(({ stage }) => ({ path: [".env", `.env.${stage}`] })),
83],
84```
85
86## Dotenv details
87
88`Secrets.DotEnv` reads one file or an ordered list. The implicit
89default `.env` may be absent; a file you name must exist. Empty
90values are real values and override earlier files. Pass
91`expandVariables: true` to expand `${OTHER}` references.
92
93Nothing is written to `process.env`. Values live in Effect's
94[ConfigProvider](https://effect.website/docs/configuration/), so two
95stacks in one process never see each other's files.
96
97`--env-file` retains its existing behavior when `secrets` is omitted.
98It is refused when the stack declares `secrets`; put
99`Secrets.DotEnv({ path })` in the list instead.
100
101## Seeing what loaded
102
103With `--log-level debug`, dotenv, Doppler, and Infisical report the
104variable names they loaded, never their values. The process environment
105reports only a count. These messages describe loaded sources; process
106environment overrides and custom layers can still change the final values.
107
108```text
109Loaded 3 secrets from dotenv (.env, .env.prod): DATABASE_URL, PORT, SENTRY_DSN
110Loaded 12 secrets from Doppler (project 'app' config 'prd'): API_KEY, DATABASE_URL, ...
111Loaded 87 secrets from the process environment
112```
113
114## Bring your own source
115
116Any Effect `Layer` that provides a `ConfigProvider` is a valid entry.
117Use `ConfigProvider.layerAdd` with `asPrimary` to merge with what
118came before, the way the built-in providers do; a plain
119`ConfigProvider.layer` replaces everything listed before it:
120
121```typescript
122secrets: [
123 ConfigProvider.layerAdd(ConfigProvider.fromUnknown(defaults), { asPrimary: true }),
124 Alchemy.Secrets.DotEnv(),
125],
126```
127
128The Doppler and Infisical providers skip remote loading during
129[auth-provider discovery](/environments/auth-providers), which builds
130the stack offline just to find out which clouds it uses.
131
132## Where next
133
134- [Doppler](/environments/doppler) — browser login locally, service tokens in CI.
135- [Infisical](/environments/infisical) — machine identities locally, platform OIDC in CI.
136- [Secrets & Config](/environments/secrets) — how a `Config` value ends up bound to a Worker or Lambda.