ConfigProvider
Data sources used by Config to load raw configuration values. A
ConfigProvider reads paths from places such as environment variables,
JavaScript objects, .env contents, or directories, and returns a uniform
Node shape that config schemas can decode. The module also includes helpers
for composing providers, changing paths, and installing providers through
layers.
Combinators
constantCase
Converts all string path segments to CONSTANT_CASE before lookup.
When to use
Use to bridge camelCase schema keys to SCREAMING_SNAKE_CASE
environment variables.
Details
Numeric segments are left unchanged. String segments use String.configCase
so numeric word groups such as v2 are preserved for environment variable
names. This is a specialization of mapInput.
See
- mapInput – for arbitrary path transformations
Signature
declare const constantCase: (self: ConfigProvider) => ConfigProviderExample
(Resolving camelCase keys to env vars)
import { ConfigProvider, Effect } from "effect"
const provider = ConfigProvider.fromEnv({ env: { DATABASE_HOST: "localhost" }}).pipe(ConfigProvider.constantCase)
// path ["databaseHost"] now resolves to env var DATABASE_HOSTconst node = Effect.runSync(provider.load(["databaseHost"]))node?.value // => "localhost"Transforms the path segments before they reach the underlying store.
When to use
Use when you need to rename, re-case, or otherwise transform config path segments before lookup.
Details
The function f receives the whole path produced by earlier provider
transformations and must return a new path. Lookup path transformations
compose in application order: the existing transformation runs first, then
f runs. For providers composed with orElse, the transformation is
applied to each operand.
The combinator delegates transformation to the provider itself. Use make for custom sources so this capability is implemented automatically.
See
- constantCase – a preset that converts to
CONSTANT_CASE - nested – for prepending a prefix instead of transforming
Signature
declare const mapInput: { (f: (path: Path) => Path): (self: ConfigProvider) => ConfigProvider; (self: ConfigProvider, f: (path: Path) => Path): ConfigProvider;}Example
(Uppercasing path segments)
import { ConfigProvider, Effect } from "effect"
const provider = ConfigProvider.fromEnv({ env: { APP_HOST: "localhost" }})
const upper = ConfigProvider.mapInput(provider, (path) => path.map((seg) => typeof seg === "string" ? seg.toUpperCase() : seg ))
const node = Effect.runSync(upper.load(["app_host"]))node?.value // => "localhost"Scopes a provider so that all lookups are prefixed with the given path segments.
When to use
Use to namespace config under a prefix like "app" or "database", or
reuse the same provider shape for multiple sub-configs.
Details
Accepts a single string or a full Path array. For providers composed with
orElse, the prefix is applied to each operand. Supports both
data-last and data-first calling conventions.
Gotchas
Ordering matters when composing with mapInput or
constantCase. Later provider transformations run after earlier ones:
a later nested becomes the outer prefix, and a later mapInput sees the
whole path produced by previous transformations.
See
- mapInput – for arbitrary path transformations
Signature
declare const nested: { (prefix: string | Path): (self: ConfigProvider) => ConfigProvider; (self: ConfigProvider, prefix: string | Path): ConfigProvider;}Example
(Nesting under a prefix)
import { ConfigProvider, Effect } from "effect"
const provider = ConfigProvider.fromEnv({ env: { APP_HOST: "localhost", APP_PORT: "3000" }})
// Lookups for ["HOST"] now resolve to ["APP", "HOST"]const scoped = ConfigProvider.nested(provider, "APP")const node = Effect.runSync(scoped.load(["HOST"]))node?.value // => "localhost"Returns a provider that falls back to that when self returns undefined
for a path.
When to use
Use to layer multiple config sources, such as env vars plus a defaults file, or provide partial overrides on top of a base config.
Details
Each provider keeps its own path transformations. If the combined provider is later transformed with mapInput or nested, the transformation is applied to both sides.
Gotchas
The fallback only runs when the path is not found (undefined). A
SourceError from self is not caught; it propagates immediately.
See
- layerAdd – install a fallback provider via a Layer
Signature
declare const orElse: { (that: ConfigProvider): (self: ConfigProvider) => ConfigProvider; (self: ConfigProvider, that: ConfigProvider): ConfigProvider;}Example
(Falling back to a default provider)
import { ConfigProvider, Effect } from "effect"
const envProvider = ConfigProvider.fromEnv({ env: { HOST: "prod.example.com" }})const defaults = ConfigProvider.fromUnknown({ HOST: "localhost", PORT: "3000" })
const combined = ConfigProvider.orElse(envProvider, defaults)
const host = Effect.runSync(combined.load(["HOST"]))const port = Effect.runSync(combined.load(["PORT"]))const values = [host?.value, port?.value] // => ["prod.example.com", "3000"]Constructors
Creates a ConfigProvider that reads configuration from a directory tree
on disk, where each file is a leaf value and each directory is a container.
When to use
Use when you expose each config key as a file under a directory, such as Kubernetes ConfigMap or Secret volume mounts.
Details
Resolution tries a regular file first and returns a Value node for
non-empty trimmed file contents. If the file read fails, it tries a directory
and returns a Record node with immediate child names as keys. If both fail
with NotFound, it returns undefined. Other platform failures return
SourceError.
Requires Path and FileSystem in the Effect context. Defaults to root
path /; override with { rootPath: "/etc/config" }.
Literal empty strings are treated as missing values by default after file
contents are trimmed. Pass { preserveEmptyStrings: true } to keep empty
strings as explicit values. Directory listings still reflect the file names
present on disk.
See
- fromEnv – for environment variables
- fromDotEnv – for
.envfiles
Signature
declare const fromDir: (options?: { readonly preserveEmptyStrings?: boolean; readonly rootPath?: string;}) => Effect.Effect<ConfigProvider, never, Path_.Path | FileSystem.FileSystem>Example
(Reading config from a directory)
import { ConfigProvider, Effect, FileSystem, Path } from "effect"
const fileSystem = FileSystem.makeNoop({ readFileString: (path) => path === "/etc/myapp/host" ? Effect.succeed("localhost") : Effect.die("unexpected path")})
const program = Effect.gen(function*() { const provider = yield* ConfigProvider.fromDir({ rootPath: "/etc/myapp" }) return yield* provider.load(["host"])})
const node = await Effect.runPromise( program.pipe( Effect.provide(Path.layer), Effect.provideService(FileSystem.FileSystem, fileSystem) ))node?.value // => "localhost"fromDotEnv
Creates a ConfigProvider by reading and parsing a .env file from the
file system.
When to use
Use to load environment config from a .env file at application startup.
Details
Requires FileSystem in the Effect context. Defaults to reading ".env" in
the current directory; override with { path: "/custom/.env" }.
Variable expansion (for example, ${VAR}) is disabled by default; enable
with { expandVariables: true }.
Literal empty strings are treated as missing values when loaded as values by
default. Pass { preserveEmptyStrings: true } to keep empty strings as
explicit values. Child discovery still reflects the keys present in the
parsed .env source.
Returns an Effect that resolves to a ConfigProvider. Fails with a
PlatformError if the file cannot be read.
See
- fromDotEnvContents – parse a
.envstring directly - fromEnv – read from the runtime environment
Signature
declare const fromDotEnv: (options?: { readonly expandVariables?: boolean; readonly path?: string; readonly preserveEmptyStrings?: boolean;}) => Effect.Effect<ConfigProvider, PlatformError, FileSystem.FileSystem>Example
(Loading a .env file)
import { ConfigProvider, Effect, FileSystem } from "effect"
const fileSystem = FileSystem.makeNoop({ readFileString: () => Effect.succeed("HOST=localhost")})
const program = Effect.gen(function*() { const provider = yield* ConfigProvider.fromDotEnv() return yield* provider.load(["HOST"])})
const node = await Effect.runPromise( Effect.provideService(program, FileSystem.FileSystem, fileSystem))node?.value // => "localhost"fromDotEnvContents
Creates a ConfigProvider by parsing the string contents of a .env file.
When to use
Use when you already have the .env contents as a string, such as contents
fetched from a remote store or embedded in a test.
Details
Supports export prefixes, single/double/backtick quoting, inline comments,
and escaped newlines. Variable expansion (for example, ${VAR}) is disabled
by default; enable with { expandVariables: true }.
Literal empty strings are treated as missing values when loaded as values by
default. Pass { preserveEmptyStrings: true } to keep empty strings as
explicit values. Child discovery still reflects the keys present in the
parsed .env source.
Parsing is based on the dotenv / dotenv-expand algorithm.
Internally delegates to fromEnvRecord with the parsed key-value pairs.
See
- fromDotEnv – loads a
.envfile from disk - fromEnvRecord – for explicit environment records
- fromEnv – for raw environment variable access
Signature
declare function fromDotEnvContents(lines: string, options?: { readonly expandVariables?: boolean; readonly preserveEmptyStrings?: boolean;}): ConfigProviderExample
(Parsing .env contents)
import { ConfigProvider, Effect } from "effect"
const contents = `HOST=localhostPORT=3000# this is a comment`
const provider = ConfigProvider.fromDotEnvContents(contents)const port = Effect.runSync(provider.load(["PORT"]))port?.value // => "3000"Creates a ConfigProvider backed by environment variables.
When to use
Use to read configuration from process.env, which is the default when no
provider is explicitly set, or pass a custom env record for testing.
Details
Path segments are joined with _ for direct lookup, and env var names are
also split on _ to build a trie for child key discovery. This means
DATABASE_HOST=localhost is accessible at both path ["DATABASE_HOST"]
and ["DATABASE", "HOST"]. If all immediate children of a trie node have
purely numeric names, the node is reported as an Array; otherwise as a
Record.
The default environment merges process.env and import.meta.env (when
available). Override by passing { env: { ... } }.
Literal empty strings are treated as missing values when loaded as values by
default. Pass { preserveEmptyStrings: true } to keep empty strings as
explicit values. Child discovery still reflects the environment variable
names present in the source.
Never fails with SourceError — all lookups are synchronous.
See
- fromUnknown – for JSON objects
- fromEnvRecord – for explicit records in restricted runtimes
- constantCase – bridge camelCase keys to SCREAMING_SNAKE_CASE
Signature
declare function fromEnv(options?: { readonly env?: Record<string, string>; readonly preserveEmptyStrings?: boolean;}): ConfigProviderExample
(Reading from a custom env record)
import { Config, ConfigProvider, Effect } from "effect"
const provider = ConfigProvider.fromEnv({ env: { DATABASE_HOST: "localhost", DATABASE_PORT: "5432" }})
const host = Config.string("HOST").parse( provider.pipe(ConfigProvider.nested("DATABASE")))
Effect.runSync(host) // => "localhost"fromEnvRecord
Creates a ConfigProvider backed by an explicit environment record.
When to use
Use when a restricted runtime cannot evaluate the automatic environment detection performed by fromEnv, or whenever the environment record must be supplied explicitly.
Details
undefined values are ignored. Path lookup and child discovery otherwise
use the same environment-variable semantics as fromEnv.
Environment variable names are captured at construction time to establish record keys and array lengths. The supplied record remains live for value lookups, so updates to known paths are observed by later loads. Keys added after construction can be loaded directly, but do not appear in captured parent record keys or array lengths.
Literal empty strings are treated as missing values by default. Pass
{ preserveEmptyStrings: true } to keep empty strings as explicit values.
See
- fromEnv – automatically reads the runtime environment
Signature
declare function fromEnvRecord(env: Record<string, string | undefined>, options?: { readonly preserveEmptyStrings?: boolean;}): ConfigProviderfromUnknown
Creates a ConfigProvider backed by an in-memory JavaScript value
(typically a parsed JSON object).
When to use
Use when you need deterministic config from an in-memory JavaScript value, such as in tests, embedded config, or parsed JSON.
Details
Path traversal follows standard JS rules: string segments index into object
keys, numeric segments index into arrays. Returns undefined for any
path that cannot be resolved. Never fails with SourceError.
Primitive values (number, boolean, bigint) are stringified via
String(...).
Literal empty strings are treated as missing values when loaded as values by
default. Pass { preserveEmptyStrings: true } to keep empty strings as
explicit values.
Gotchas
Object keys and array lengths reflect the original input shape. A leaf value
of "" is treated as missing when that leaf is loaded, but the parent
container still reports its original keys or length.
See
Signature
declare function fromUnknown(root: unknown, options?: { readonly preserveEmptyStrings?: boolean;}): ConfigProviderExample
(Providing config from a plain object)
import { Config, ConfigProvider, Effect } from "effect"
const provider = ConfigProvider.fromUnknown({ database: { host: "localhost", port: 5432 }})
const host = Config.string("host").parse( provider.pipe(ConfigProvider.nested("database")))
Effect.runSync(host) // => "localhost"Creates a ConfigProvider from a raw lookup function.
When to use
Use when implementing a provider backed by a custom store, such as a database, remote API, or in-memory map.
Details
The get callback receives a Path and must return
Effect<Node | undefined, SourceError>. Return undefined when the path does
not exist, a Node when it does, and fail with SourceError only when the
source cannot be read.
Providers created by make also implement the path-transformation
capability used by mapInput, constantCase, and
nested.
See
- fromEnv – pre-built provider for environment variables
- fromUnknown – pre-built provider for JSON objects
Signature
declare function make(get: (path: Path) => Effect<Node | undefined, SourceError>): ConfigProviderExample
(Creating a simple in-memory provider)
import { ConfigProvider, Effect } from "effect"
const data: Record<string, string> = { host: "localhost", port: "5432"}
const provider = ConfigProvider.make((path) => { const key = path.join(".") const value = data[key] return Effect.succeed( value !== undefined ? ConfigProvider.makeValue(value) : undefined )})
Effect.runSync(provider.load(["host"])) // => ConfigProvider.makeValue("localhost")Creates an Array node representing an indexed container with a known
length.
When to use
Use when you need to describe a JSON array or numerically indexed env vars inside a custom provider.
Details
The optional value allows a node to be both a container and a leaf at the
same time.
See
- makeValue – for terminal leaves
- makeRecord – for object-like containers
Signature
declare function makeArray(length: number, value?: string): NodeExample
(Creating an array node)
import { ConfigProvider } from "effect"
ConfigProvider.makeArray(3) // => { _tag: "Array", length: 3, value: undefined }makeRecord
Creates a Record node representing an object-like container with known
child keys.
When to use
Use when you need to describe a directory or JSON object inside a custom provider.
Details
The optional value allows a node to be both a container and a leaf at the
same time (for example, an env var A=x that also has children A_FOO and
A_BAR).
See
Signature
declare function makeRecord(keys: ReadonlySet<string>, value?: string): NodeExample
(Creating a record node)
import { ConfigProvider } from "effect"
const node = ConfigProvider.makeRecord(new Set(["host", "port"]))node._tag // => "Record"if (node._tag === "Record") { node.keys // => new Set(["host", "port"]) node.value // => undefined}Creates a Value node representing a terminal string leaf.
When to use
Use when building nodes inside a custom ConfigProvider's get
callback.
Details
The function returns a new plain object.
See
- makeRecord – for object-like containers
- makeArray – for array-like containers
Signature
declare function makeValue(value: string): NodeExample
(Creating a value node)
import { ConfigProvider } from "effect"
ConfigProvider.makeValue("3000") // => { _tag: "Value", value: "3000" }Errors
SourceError
Typed error indicating that a configuration source could not be read.
When to use
Use when you need to report that a custom provider's underlying store is unreachable or produced an I/O error while reading configuration data.
Gotchas
Do not use SourceError for "key not found". That case is represented by
returning undefined from load.
See
- ConfigProvider – the interface whose
loadmay fail with this error
Signature
declare class SourceError extends YieldableError<this> & { readonly _tag: "SourceError";} & Readonly<{ readonly cause?: unknown; readonly message: string;}> { constructor(args: { readonly cause?: unknown; readonly message: string; });}Example
(Failing with a SourceError)
import { ConfigProvider, Effect } from "effect"
const provider = ConfigProvider.make((_path) => Effect.fail( new ConfigProvider.SourceError({ message: "connection refused" }) ))
Effect.runSync(Effect.flip(provider.load(["host"]))).message // => "connection refused"Layers
Provides a layer that installs a ConfigProvider as the active provider for
all downstream effects, replacing any previously installed provider.
When to use
Use to set the config source for an entire application or test suite.
Details
Accepts either a plain ConfigProvider or an Effect that produces one.
When given an Effect, it is evaluated once when the layer is built.
See
- layerAdd – add a provider without replacing the existing one
Signature
declare function layer<E = never, R = never>(self: ConfigProvider | Effect<ConfigProvider, E, R>): Layer<never, E, Exclude<R, Scope>>Example
(Reading config from a JSON object)
import { Config, ConfigProvider, Effect, Layer } from "effect"
const TestLayer = ConfigProvider.layer( ConfigProvider.fromUnknown({ port: 8080 }))
const program = Effect.gen(function*() { const port = yield* Config.number("port") return port})
Effect.runSync(Effect.provide(program, TestLayer)) // => 8080Creates a Layer that composes a new ConfigProvider with the currently
active one, rather than replacing it.
When to use
Use to add defaults that should only apply when the primary provider has no
value for a path, or override specific keys while keeping the rest from the
existing provider by setting asPrimary: true.
Details
By default, the new provider acts as a fallback and is consulted only when
the current provider returns undefined. Set asPrimary: true to make
the new provider the primary source, with the existing one as fallback.
See
Signature
declare function layerAdd<E = never, R = never>(self: ConfigProvider | Effect<ConfigProvider, E, R>, options?: { readonly asPrimary?: boolean;}): Layer<never, E, Exclude<R, Scope>>Example
(Adding default values)
import { Config, ConfigProvider, Effect, Layer } from "effect"
const defaults = ConfigProvider.fromUnknown({ HOST: "localhost", PORT: "3000"})
// The current env provider is tried first; `defaults` is the fallbackconst DefaultsLayer = ConfigProvider.layerAdd(defaults)const BaseLayer = ConfigProvider.layer(ConfigProvider.fromUnknown({}))const program = Config.string("HOST")
const layer = Layer.provide(DefaultsLayer, BaseLayer)Effect.runSync(Effect.provide(program, layer)) // => "localhost"Models
A discriminated union describing the shape of a configuration value at a given path.
When to use
Use when implementing a custom ConfigProvider by returning raw
nodes from the get callback passed to make, or when inspecting raw
provider output before schema parsing.
Details
Value is a terminal string leaf. Record is an object-like container
whose immediate child keys are known and may carry an optional co-located
value. Array is an indexed container with a known length and may also
carry an optional co-located value.
Provider lookups return undefined when no node exists at the requested
path. Within a node that was found, value: undefined has a narrower
structural meaning: the container exists but has no co-located scalar value.
See
- makeValue – construct a
Valuenode - makeRecord – construct a
Recordnode - makeArray – construct an
Arraynode
Signature
type Node = { readonly _tag: "Value"; readonly value: string;} | { readonly _tag: "Record"; readonly keys: ReadonlySet<string>; readonly value: string | undefined;} | { readonly _tag: "Array"; readonly length: number; readonly value: string | undefined;}An ordered sequence of string or numeric segments that addresses a node in the configuration tree. String segments name object keys; numeric segments index into arrays.
When to use
Use to address raw configuration nodes when implementing or transforming a
ConfigProvider.
Signature
type Path = ReadonlyArray<string | number>Example
(A typical config path)
import type { ConfigProvider } from "effect"
const path: ConfigProvider.Path = ["database", "replicas", 0, "host"]path.join(".") // => "database.replicas.0.host"Services
ConfigProvider
Context reference for the active raw configuration provider, registered in the context with a
default value of fromEnv(). Because it is a Context.Reference, it is
available without explicit provision; Config schemas automatically resolve
it.
When to use
Use to override the active raw configuration provider for an entire program, or retrieve the current provider inside an Effect.
See
Signature
declare const ConfigProvider: Reference<ConfigProvider>Example
(Providing a custom provider)
import { ConfigProvider, Effect } from "effect"
const provider = ConfigProvider.fromUnknown({ port: 8080 })
const program = Effect.gen(function*() { const current = yield* ConfigProvider.ConfigProvider return current}).pipe( Effect.provideService(ConfigProvider.ConfigProvider, provider))
Effect.runSync(program) === provider // => trueConfigProvider interface
The core interface for loading raw configuration data.
When to use
Use to type-annotate variables that hold a provider or to implement a custom provider via make.
Details
load(path) is the semantic lookup operation used by the Config module.
It applies provider transformations and composition before consulting the
underlying source. undefined means "not found", a Node means the path
exists, and SourceError means the source itself failed.
mapInput(f) is the provider's path-transformation capability. Keeping this
capability on the provider allows source and composite providers to preserve
their own lookup behavior without exposing an internal representation.
Transformations compose in application order: f receives the path produced
by earlier transformations.
load deliberately accepts only a Path. Path transformation is modeled by
returning another provider through mapInput, rather than by adding a
transformation callback to every lookup. Custom implementations therefore
expose lookup and transformation behavior, but no source or composition
state.
See
Signature
interface ConfigProvider extends Pipeable { readonly load: (path: Path) => Effect<Node | undefined, SourceError>; readonly mapInput: (f: (path: Path) => Path) => ConfigProvider;}