envil envil 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/envil"
import { fromAwsSecrets } from "@ayronforge/envil/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. Resolver initialization/configuration failures surface as ResolverError in the Effect error channel
  5. Per-secret fetch misses are returned as undefined and then handled by schema validation

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.

Resolver failure modes

Built-in resolvers default to lenient per-secret fetching: if an individual fetch fails, the resolver returns undefined for that key and schema validation decides whether that is acceptable.

Set strict: true on a resolver to fail immediately with ResolverError on fetch/parsing errors:

fromAwsSecrets({
  secrets: { DB_PASS: "prod/db-pass" },
  strict: true,
})

Available resolvers

Name Type Default Description
fromAwsSecrets @ayronforge/envil/aws AWS Secrets Manager. Peer dep: @aws-sdk/client-secrets-manager
fromGcpSecrets @ayronforge/envil/gcp GCP Secret Manager. Peer dep: @google-cloud/secret-manager
fromAzureKeyVault @ayronforge/envil/azure Azure Key Vault. Peer deps: @azure/keyvault-secrets, @azure/identity
fromOnePassword @ayronforge/envil/1password 1Password. Peer dep: @1password/sdk
fromRemoteSecrets @ayronforge/envil 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.