1---
2title: Secrets & env
3description: Deliver API keys from .env to a Lambda with effect/Config, and graduate to AWS Secrets Manager when the secret is a shared, generated, or rotated cloud resource.
4---
5
6Secrets on AWS come in two tiers. Values in your `.env` that only
7your own code reads travel to the Lambda as environment variables
8via `effect/Config` — one line binds at deploy time and resolves at
9runtime. When the secret is a cloud resource other things consume,
10it belongs in [Secrets Manager](#graduate-to-secrets-manager). The
11rule: in your `.env` and read only by your code → `Config`;
12shared, generated, or rotated → Secrets Manager.
13
14## Bind a secret from .env
15
16`yield* Config.Redacted(...)` in the function's Construction phase reads
17your `.env` at deploy time and binds the value as a Lambda
18environment variable:
19
20```typescript
21// src/api.ts
22import * as AWS from "alchemy/AWS";
23import * as Config from "effect/Config";
24import * as Effect from "effect/Effect";
25import * as HttpServerResponse from "effect/http/HttpServerResponse";
26
27export default class Api extends AWS.Lambda.Function<Api>()(
28 "Api",
29 { main: import.meta.url, functionUrl: true },
30 Effect.gen(function* () {
31 const apiKey = yield* Config.Redacted("OPENAI_API_KEY");
32
33 return {
34 fetch: Effect.gen(function* () {
35 return HttpServerResponse.text("ok");
36 }),
37 };
38 }),
39) {}
40```
41
42At runtime the same line resolves from that env var — one line,
43both phases.
44
45## Unwrap at the call site
46
47The value is `Redacted<string>`; unwrap with `Redacted.value` only
48where the raw string is needed:
49
50```typescript
51fetch: Effect.gen(function* () {
52 return HttpServerResponse.text(`Bearer ${Redacted.value(apiKey)}`);
53}),
54```
55
56Interpolating the wrapper itself (`` `Bearer ${apiKey}` ``) produces
57the literal text `<redacted>`, not the value — that's the point.
58
59## Plain vars and defaults
60
61Non-secret config uses the same mechanism with `Config.String` and
62`Config.Number`:
63
64```typescript
65const host = yield* Config.String("HOST");
66const port = yield* Config.Number("PORT").pipe(Config.withDefault(3000));
67```
68
69Values JSON round-trip through the environment, so `port` comes
70back as a number. Combinators re-run at runtime against the bound
71source, and a default is never bound — see
72[Secrets and Config](/environments/secrets) for the full semantics.
73
74## Resolve Config in the constructor, not in fetch
75
76A `Config` yielded only inside `fetch` is never discovered at
77deploy time, so the env var won't exist at runtime:
78
79```typescript
80export default class Api extends AWS.Lambda.Function<Api>()(
81 "Api",
82 { main: import.meta.url, functionUrl: true },
83 Effect.gen(function* () {
84 return {
85 fetch: Effect.gen(function* () {
86 // 🚫 never bound — API_KEY won't exist at runtime
87 const apiKey = yield* Config.Redacted("API_KEY");
88 }),
89 };
90 }),
91) {}
92```
93
94Resolve in the outer `Effect.gen`, capture in a `const`, reference
95that `const` from `fetch`.
96
97## Graduate to Secrets Manager
98
99Reach for Secrets Manager when the secret is an AWS-native concern:
100
101- **Shared consumers** — other resources reference the secret by
102 ARN (Aurora clusters and RDS proxies consume password-backed JSON
103 secrets in exactly the shape `generateSecretString` produces).
104- **Generated values** — the plaintext never enters your repo,
105 `.env`, or deploy logs.
106- **Least-privilege reads** — access is an IAM grant on one ARN,
107 not an env var visible to the whole function config.
108- **Rotation without redeploy** — put a new value and running
109 functions read it on their next call.
110
111## Create a secret
112
113`Secret` owns the metadata and current value; pass the value as
114`Redacted` so it never appears in logs or state:
115
116```typescript
117import * as AWS from "alchemy/AWS";
118import * as Redacted from "effect/Redacted";
119
120const dbSecret = yield* AWS.SecretsManager.Secret("DbSecret", {
121 description: "Database credentials",
122 secretString: Redacted.make(
123 JSON.stringify({ username: "app", password: "super-secret" }),
124 ),
125});
126```
127
128Omit `name` for a deterministic generated name; changing `name`
129replaces the secret (new ARN), everything else updates in place.
130On destroy alchemy deletes with `ForceDeleteWithoutRecovery` —
131removal is immediate, no recovery window.
132
133## Generate the value instead
134
135For credentials nothing outside AWS needs to see, let Secrets
136Manager mint the password at deploy time:
137
138```typescript
139const dbSecret = yield* AWS.SecretsManager.Secret("DbSecret", {
140 generateSecretString: {
141 secretStringTemplate: JSON.stringify({ username: "app" }),
142 generateStringKey: "password",
143 PasswordLength: 32,
144 },
145});
146```
147
148The stored value is the template merged with the generated password
149— `{"username":"app","password":"<random>"}` — and the plaintext
150never lives in your repo, `.env`, or logs.
151
152## Read it from a Lambda
153
154`GetSecretValue` is a binding: yield it in the constructor to grant IAM at
155deploy time, call it at runtime for a fresh read:
156
157```typescript
158// src/api.ts
159import * as AWS from "alchemy/AWS";
160import * as Effect from "effect/Effect";
161import * as HttpServerResponse from "effect/http/HttpServerResponse";
162import * as Redacted from "effect/Redacted";
163
164export default class Api extends AWS.Lambda.Function<Api>()(
165 "Api",
166 { main: import.meta.url, functionUrl: true },
167 Effect.gen(function* () {
168 const dbSecret = yield* AWS.SecretsManager.Secret("DbSecret", {
169 generateSecretString: {
170 secretStringTemplate: JSON.stringify({ username: "app" }),
171 generateStringKey: "password",
172 },
173 });
174
175 const getDbSecret = yield* AWS.SecretsManager.GetSecretValue(dbSecret);
176
177 return {
178 fetch: Effect.gen(function* () {
179 const value = yield* getDbSecret();
180 const creds = Redacted.isRedacted(value.SecretString)
181 ? (JSON.parse(Redacted.value(value.SecretString)) as {
182 username: string;
183 })
184 : undefined;
185 return yield* HttpServerResponse.json({ user: creds?.username });
186 }).pipe(Effect.orDie),
187 };
188 }).pipe(Effect.provide(AWS.SecretsManager.GetSecretValueHttp)),
189) {}
190```
191
192The binding adds `secretsmanager:GetSecretValue` and
193`secretsmanager:DescribeSecret` to the execution role, scoped to
194this secret's ARN only. `SecretString` comes back as `Redacted` —
195unwrap with `Redacted.value` at the point of use. The value is
196fetched per call, not baked into env, so putting a new version (the
197[`PutSecretValue` binding](/providers/aws/secretsmanager/putsecretvalue))
198propagates on the next read.
199
200## Where next
201
202Related:
203
204- [RDS](/aws/data/rds) — Aurora generates a password-backed secret in
205 exactly the `generateSecretString` shape; the `Connect` binding
206 reads it at request time.
207- [Lambda](/aws/compute/lambda) — the function model env vars and bindings
208 attach to.
209- [Secrets and Config](/environments/secrets) — the Construction/Runtime split
210 and transformation semantics.
211- [Secrets & env on Cloudflare](/cloudflare/security/secrets-env) — the same
212 spine on Cloudflare.
213
214Reference:
215
216- [`Secret` reference](/providers/aws/secretsmanager/secret)
217- [`GetSecretValue` reference](/providers/aws/secretsmanager/getsecretvalue)