Secrets & env
Secrets on AWS come in two tiers. Values in your .env that only
your own code reads travel to the Lambda as environment variables
via effect/Config — one line binds at deploy time and resolves at
runtime. When the secret is a cloud resource other things consume,
it belongs in Secrets Manager. The
rule: in your .env and read only by your code → Config;
shared, generated, or rotated → Secrets Manager.
Bind a secret from .env
Section titled “Bind a secret from .env”yield* Config.redacted(...) in the function’s init phase reads
your .env at deploy time and binds the value as a Lambda
environment variable:
import * as AWS from "alchemy/AWS";import * as Config from "effect/Config";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
export default class Api extends AWS.Lambda.Function<Api>()( "Api", { main: import.meta.url, url: true }, Effect.gen(function* () { const apiKey = yield* Config.redacted("OPENAI_API_KEY");
return { fetch: Effect.gen(function* () { return HttpServerResponse.text("ok"); }), }; }),) {}At runtime the same line resolves from that env var — one line, both phases.
Unwrap at the call site
Section titled “Unwrap at the call site”The value is Redacted<string>; unwrap with Redacted.value only
where the raw string is needed:
fetch: Effect.gen(function* () { return HttpServerResponse.text(`Bearer ${Redacted.value(apiKey)}`);}),Interpolating the wrapper itself (`Bearer ${apiKey}`) produces
the literal text <redacted>, not the value — that’s the point.
Plain vars and defaults
Section titled “Plain vars and defaults”Non-secret config uses the same mechanism with Config.string and
Config.number:
const host = yield* Config.string("HOST");const port = yield* Config.number("PORT").pipe(Config.withDefault(3000));Values JSON round-trip through the environment, so port comes
back as a number. Combinators re-run at runtime against the bound
source, and a default is never bound — see
Secrets and Config for the full semantics.
Resolve Config in init, not in fetch
Section titled “Resolve Config in init, not in fetch”A Config yielded only inside fetch is never discovered at
deploy time, so the env var won’t exist at runtime:
export default class Api extends AWS.Lambda.Function<Api>()( "Api", { main: import.meta.url, url: true }, Effect.gen(function* () { return { fetch: Effect.gen(function* () { // 🚫 never bound — API_KEY won't exist at runtime const apiKey = yield* Config.redacted("API_KEY"); }), }; }),) {}Resolve in the outer Effect.gen, capture in a const, reference
that const from fetch.
Graduate to Secrets Manager
Section titled “Graduate to Secrets Manager”Reach for Secrets Manager when the secret is an AWS-native concern:
- Shared consumers — other resources reference the secret by
ARN (Aurora clusters and RDS proxies consume password-backed JSON
secrets in exactly the shape
generateSecretStringproduces). - Generated values — the plaintext never enters your repo,
.env, or deploy logs. - Least-privilege reads — access is an IAM grant on one ARN, not an env var visible to the whole function config.
- Rotation without redeploy — put a new value and running functions read it on their next call.
Create a secret
Section titled “Create a secret”Secret owns the metadata and current value; pass the value as
Redacted so it never appears in logs or state:
import * as AWS from "alchemy/AWS";import * as Redacted from "effect/Redacted";
const dbSecret = yield* AWS.SecretsManager.Secret("DbSecret", { description: "Database credentials", secretString: Redacted.make( JSON.stringify({ username: "app", password: "super-secret" }), ),});Omit name for a deterministic generated name; changing name
replaces the secret (new ARN), everything else updates in place.
On destroy alchemy deletes with ForceDeleteWithoutRecovery —
removal is immediate, no recovery window.
Generate the value instead
Section titled “Generate the value instead”For credentials nothing outside AWS needs to see, let Secrets Manager mint the password at deploy time:
const dbSecret = yield* AWS.SecretsManager.Secret("DbSecret", { generateSecretString: { secretStringTemplate: JSON.stringify({ username: "app" }), generateStringKey: "password", PasswordLength: 32, },});The stored value is the template merged with the generated password
— {"username":"app","password":"<random>"} — and the plaintext
never lives in your repo, .env, or logs.
Read it from a Lambda
Section titled “Read it from a Lambda”GetSecretValue is a binding: yield it in init to grant IAM at
deploy time, call it at runtime for a fresh read:
import * as AWS from "alchemy/AWS";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";import * as Redacted from "effect/Redacted";
export default class Api extends AWS.Lambda.Function<Api>()( "Api", { main: import.meta.url, url: true }, Effect.gen(function* () { const dbSecret = yield* AWS.SecretsManager.Secret("DbSecret", { generateSecretString: { secretStringTemplate: JSON.stringify({ username: "app" }), generateStringKey: "password", }, });
const getDbSecret = yield* AWS.SecretsManager.GetSecretValue(dbSecret);
return { fetch: Effect.gen(function* () { const value = yield* getDbSecret(); const creds = Redacted.isRedacted(value.SecretString) ? (JSON.parse(Redacted.value(value.SecretString)) as { username: string; }) : undefined; return yield* HttpServerResponse.json({ user: creds?.username }); }).pipe(Effect.orDie), }; }).pipe(Effect.provide(AWS.SecretsManager.GetSecretValueHttp)),) {}The binding adds secretsmanager:GetSecretValue and
secretsmanager:DescribeSecret to the execution role, scoped to
this secret’s ARN only. SecretString comes back as Redacted —
unwrap with Redacted.value at the point of use. The value is
fetched per call, not baked into env, so putting a new version (the
PutSecretValue binding)
propagates on the next read.
Where next
Section titled “Where next”Related:
- RDS — Aurora generates a password-backed secret in
exactly the
generateSecretStringshape; theConnectbinding reads it at request time. - Lambda — the function model env vars and bindings attach to.
- Secrets and Config — the init/runtime split and transformation semantics.
- Secrets & env on Cloudflare — the same spine on Cloudflare.
Reference: