better-env better-env Docs

Resolvers Overview

Resolve environment variables from cloud secret managers at startup.

Resolvers let you pull environment variable values from cloud secret managers instead of (or in addition to) process.env. This is useful in production environments where secrets are stored in services like AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, or 1Password.

How resolvers work

When you pass a resolvers array to createEnv, the return type changes from a plain object to an Effect.Effect:

import { createEnv, requiredString } from "@ayronforge/better-env"
import { fromAwsSecrets } from "@ayronforge/better-env/aws"
import { Effect } from "effect"

// With resolvers → returns Effect
const envEffect = createEnv({
  server: {
    DATABASE_URL: requiredString,
    API_KEY: requiredString,
  },
  resolvers: [
    fromAwsSecrets({
      secrets: {
        DATABASE_URL: "prod/database-url",
        API_KEY: "prod/api-key",
      },
    }),
  ],
})

// Run the Effect to get the env object
const env = await Effect.runPromise(envEffect)

Resolution flow

  1. All resolvers run concurrently (unbounded concurrency)
  2. Results are merged: process.env is the base, resolver results override
  3. The merged env is passed through schema validation
  4. If any resolver fails, a ResolverError is propagated through the Effect error channel

Merge behavior

Resolver results are merged left-to-right on top of process.env (or runtimeEnv):

// Final env = { ...process.env, ...resolver1Results, ...resolver2Results }
resolvers: [resolver1, resolver2]

Later resolvers override earlier ones for the same key.

Auto-redaction

By default, all values provided by resolvers are automatically wrapped in Effect’s Redacted type. This means secrets fetched from cloud providers are safe from accidental leaks through logging, serialization, or spreading.

const env = await Effect.runPromise(
  createEnv({
    server: {
      DB_HOST: requiredString,
      DB_PASS: requiredString,
    },
    resolvers: [
      fromAwsSecrets({
        secrets: { DB_PASS: "prod/db-password" },
      }),
    ],
    runtimeEnv: { DB_HOST: "localhost" },
  }),
)

// DB_PASS from resolver → Redacted<string> (type-safe)
console.log(env.DB_PASS)         // <redacted>
JSON.stringify(env)               // {"DB_HOST":"localhost","DB_PASS":"<redacted>"}
Redacted.value(env.DB_PASS)      // "actual-password"

// DB_HOST from runtimeEnv → string (not redacted)
console.log(env.DB_HOST)         // "localhost"

Types are inferred automatically: resolver-provided keys are typed as Redacted<T>, while non-resolver keys remain T.

Disabling auto-redaction

Set autoRedactResolver: false to disable automatic wrapping:

const env = await Effect.runPromise(
  createEnv({
    server: { DB_PASS: requiredString },
    resolvers: [fromAwsSecrets({ secrets: { DB_PASS: "prod/db-pass" } })],
    autoRedactResolver: false,
  }),
)

env.DB_PASS // string (not Redacted)
Tip

Even with autoRedactResolver: false, you can still use redacted() in your schema to explicitly mark individual keys as Redacted.

Available resolvers

Name Type Default Description
fromAwsSecrets @ayronforge/better-env/aws AWS Secrets Manager. Peer dep: @aws-sdk/client-secrets-manager
fromGcpSecrets @ayronforge/better-env/gcp GCP Secret Manager. Peer dep: @google-cloud/secret-manager
fromAzureKeyVault @ayronforge/better-env/azure Azure Key Vault. Peer deps: @azure/keyvault-secrets, @azure/identity
fromOnePassword @ayronforge/better-env/1password 1Password. Peer dep: @1password/sdk
fromRemoteSecrets @ayronforge/better-env Custom / remote secrets. No peer dep — bring your own client.
Note

Each built-in resolver dynamically imports its cloud SDK only when needed. You must install the peer dependency for the resolver you use. fromRemoteSecrets requires no peer dependencies — you provide the client directly.