better-env better-env Docs

Custom / Remote Secrets

Generic resolver for custom secret providers, testing, and unsupported integrations.

Overview

fromRemoteSecrets is the generic escape hatch for custom integrations, testing, and secret providers not covered by the built-in resolvers. You provide your own SecretClient implementation.

import { createEnv, requiredString, fromRemoteSecrets } from "@ayronforge/better-env"
import type { SecretClient } from "@ayronforge/better-env"
import { Effect } from "effect"

const client: SecretClient = {
  getSecret: async (id) => {
    // Fetch from your custom provider
    const response = await fetch(`https://secrets.example.com/v1/${id}`)
    if (!response.ok) return undefined
    return response.text()
  },
}

const envEffect = createEnv({
  server: {
    DATABASE_URL: requiredString,
    API_KEY: requiredString,
  },
  resolvers: [
    fromRemoteSecrets({
      secrets: {
        DATABASE_URL: "prod/database-url",
        API_KEY: "prod/api-key",
      },
      client,
    }),
  ],
})

const env = await Effect.runPromise(envEffect)

No peer dependencies required — fromRemoteSecrets is exported directly from @ayronforge/better-env.

Options

Name Type Default Description
secrets Required Record<string, string> Map of env var names to secret identifiers passed to the client.
client Required SecretClient Your custom client implementing the SecretClient interface.

SecretClient interface

interface SecretClient {
  getSecret: (secretId: string) => Promise<string | undefined>
  getSecrets?: (secretIds: string[]) => Promise<Map<string, string | undefined>>
}
  • getSecret — required. Fetches a single secret by ID.
  • getSecrets — optional. When provided and there are multiple secrets to resolve, the resolver uses this for batch fetching instead of making concurrent getSecret calls.

Batch optimization

If your SecretClient implements getSecrets, the resolver automatically uses it when resolving more than one secret. For a single secret, getSecret is always used.

const batchClient: SecretClient = {
  getSecret: async (id) => {
    const res = await fetch(`https://secrets.example.com/v1/${id}`)
    return res.ok ? res.text() : undefined
  },
  getSecrets: async (ids) => {
    const res = await fetch("https://secrets.example.com/v1/batch", {
      method: "POST",
      body: JSON.stringify({ ids }),
    })
    const data = await res.json()
    return new Map(Object.entries(data))
  },
}