Config Providers
A ConfigProvider is the backing data source that a Config
reads from. It knows how to resolve a path (like ["database", "host"]) to a
raw value. By default Effect uses a provider backed by environment variables, but
you can swap in JSON, a .env file, a directory of files, or a custom source —
without touching the Config definitions that consume it.
import { Config, ConfigProvider, Effect } from "effect"
const program = Effect.gen(function* () { const host = yield* Config.string("HOST") const port = yield* Config.port("PORT") yield* Effect.log(`${host}:${port}`)})
// Provide a fixed in-memory provider instead of reading the real environmentconst provider = ConfigProvider.fromEnv({ env: { HOST: "localhost", PORT: "8080" }})
Effect.runFork( program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)))ConfigProvider.ConfigProvider is a Context.Reference whose default value is
fromEnv(). Because it is a reference, your program works without any explicit
provision — and you override it for an entire program by providing a different
one.
Mental model
Section titled “Mental model”A provider exposes a uniform Node interface that the Config module consumes:
Node— a discriminated union (Value | Record | Array) describing what lives at a path.Path— an array ofstring | numbersegments addressing a node. String segments name object keys; numbers index into arrays.load(path)— resolves a path toEffect<Node | undefined, SourceError>.undefinedmeans not found;SourceErrormeans the source itself failed.
Built-in sources
Section titled “Built-in sources”| Provider | Reads from |
|---|---|
ConfigProvider.fromEnv(options?) | environment variables (default; process.env) |
ConfigProvider.fromUnknown(root) | an in-memory JS value / parsed JSON object |
ConfigProvider.fromDotEnvContents(s) | the string contents of a .env file |
ConfigProvider.fromDotEnv(options?) | a .env file on disk (requires FileSystem) |
ConfigProvider.fromDir(options?) | a directory tree, file-per-key (requires FileSystem, Path) |
ConfigProvider.make(get) | a custom lookup function |
ConfigProvider.fromEnv
Section titled “ConfigProvider.fromEnv”Backed by environment variables. Path segments are joined with _ for direct
lookup, and env var names are also split on _ to discover nested keys — so
DATABASE_HOST=localhost is reachable both at the flat path ["DATABASE_HOST"]
and the nested path ["DATABASE", "HOST"]. Defaults to merging process.env
and import.meta.env; pass { env } to override. Never fails with SourceError.
import { Config, ConfigProvider, Effect } from "effect"
const provider = ConfigProvider.fromEnv({ env: { DATABASE_HOST: "localhost", DATABASE_PORT: "5432" }})
const program = Effect.gen(function* () { // nested("DATABASE") makes these read DATABASE_HOST / DATABASE_PORT const db = yield* Config.all({ host: Config.string("HOST"), port: Config.port("PORT") }).pipe(Config.nested("DATABASE"))
yield* Effect.log(`${db.host}:${db.port}`) // => 0:00:00.000 localhost:5432}).pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider))ConfigProvider.fromUnknown
Section titled “ConfigProvider.fromUnknown”Backed by any in-memory JavaScript value, typically a parsed JSON object. String
segments index into object keys, numeric segments index into arrays, so it
handles arbitrarily nested config naturally. Primitives (number, boolean,
bigint) are stringified via String(...). Returns undefined for unresolved
paths and never fails with SourceError.
import { Config, ConfigProvider, Effect } from "effect"
const provider = ConfigProvider.fromUnknown({ server: { host: "localhost", port: 8080 }, replicas: [{ host: "r1" }, { host: "r2" }]})
const host = Config.string("host").parse( provider.pipe(ConfigProvider.nested("server")))
// Effect.runSync(host) // => "localhost"
// numeric segments index into the arrayconst replica = Config.string("host").parse( provider.pipe(ConfigProvider.nested(["replicas", 1])))// Effect.runSync(replica) // => "r2"ConfigProvider.fromDotEnvContents
Section titled “ConfigProvider.fromDotEnvContents”Parses the string contents of a .env file into a provider (internally
delegating to fromEnv). Supports export prefixes, single/double/backtick
quoting, inline comments, and escaped newlines. Variable expansion (${VAR}) is
disabled by default; enable with { expandVariables: true }.
import { Config, ConfigProvider } from "effect"
const provider = ConfigProvider.fromDotEnvContents(`export HOST=localhostPORT=3000# this is a commentGREETING="hello\\nworld"`)
// Config.string("HOST").parse(provider) // => "localhost"import { ConfigProvider } from "effect"
// expandVariables resolves ${...} references and ${VAR:-default} fallbacksconst provider = ConfigProvider.fromDotEnvContents( `HOST=localhost\nURL=http://\${HOST}:3000`, { expandVariables: true })// Config.string("URL").parse(provider) // => "http://localhost:3000"The only option is expandVariables?: boolean.
ConfigProvider.fromDotEnv
Section titled “ConfigProvider.fromDotEnv”Reads and parses a .env file from disk. Requires a FileSystem in context
(from your platform layer) and returns an Effect that produces the provider.
Defaults to reading ".env"; override with { path }. Fails with a
PlatformError if the file cannot be read.
import { ConfigProvider, Effect } from "effect"
const program = Effect.gen(function* () { // Reads ".env" by default; pass { path } / { expandVariables } to override const provider = yield* ConfigProvider.fromDotEnv()
return yield* myApp.pipe( Effect.provideService(ConfigProvider.ConfigProvider, provider) )})// program: Effect<void, PlatformError, FileSystem.FileSystem>
declare const myApp: Effect.Effect<void>ConfigProvider.fromDir
Section titled “ConfigProvider.fromDir”Reads a directory tree where each file is a leaf value and each directory is a
container. This matches how Kubernetes mounts ConfigMaps and Secrets — one file
per key under a mount path. A file resolves to a Value with trimmed contents;
a directory resolves to a Record of child names. Requires Path and
FileSystem; defaults to root path "/".
import { ConfigProvider, Effect } from "effect"
const program = Effect.gen(function* () { // /etc/myapp/database/host (file) → path ["database", "host"] const provider = yield* ConfigProvider.fromDir({ rootPath: "/etc/myapp" })
return yield* myApp.pipe( Effect.provideService(ConfigProvider.ConfigProvider, provider) )})// program: Effect<void, never, Path.Path | FileSystem.FileSystem>
declare const myApp: Effect.Effect<void>ConfigProvider.make
Section titled “ConfigProvider.make”Builds a provider from a raw lookup function get: (path) => Effect<Node | undefined, SourceError>.
Return a Node for a found path, undefined for not found, and fail with
SourceError only for genuine I/O errors. The optional mapInput and prefix
arguments are wired into the resulting load (combinators below use them).
import { ConfigProvider, Effect } from "effect"
// Back a provider with an arbitrary in-memory storeconst 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 )})// Config.string("host").parse(provider) // => "localhost"Path transforms and composition
Section titled “Path transforms and composition”Providers are values you can combine and reshape. Each combinator below supports
both data-first (fn(self, ...)) and data-last (self.pipe(fn(...))) styles.
ConfigProvider.orElse
Section titled “ConfigProvider.orElse”Returns a provider that falls back to that when self returns undefined for
a path. The fallback only runs on not found — a SourceError from self
propagates and is not caught.
import { Config, ConfigProvider } from "effect"
const env = ConfigProvider.fromEnv({ env: { HOST: "prod.example.com" } })const file = ConfigProvider.fromUnknown({ HOST: "localhost", PORT: "3000" })
// env first; file provides anything env is missing (e.g. PORT)const combined = ConfigProvider.orElse(env, file)
// Config.string("HOST").parse(combined) // => "prod.example.com"// Config.string("PORT").parse(combined) // => "3000"ConfigProvider.mapInput
Section titled “ConfigProvider.mapInput”Rewrites path segments before they reach the underlying store. If the provider
already has a mapInput, the functions compose — the existing mapping runs
first, then f.
import { ConfigProvider } from "effect"
const provider = ConfigProvider.fromEnv({ env: { APP_HOST: "localhost" } })
// Uppercase string segments before lookupconst upper = ConfigProvider.mapInput(provider, (path) => path.map((seg) => (typeof seg === "string" ? seg.toUpperCase() : seg)))// path ["app_host"] now resolves to env var APP_HOSTConfigProvider.constantCase
Section titled “ConfigProvider.constantCase”A preset of mapInput that converts string path segments to CONSTANT_CASE
(numeric segments are left unchanged). Bridges camelCase schema keys to
SCREAMING_SNAKE_CASE env vars.
import { Config, ConfigProvider, Effect } from "effect"
const provider = ConfigProvider.fromEnv({ env: { DATABASE_HOST: "localhost" }}).pipe(ConfigProvider.constantCase)
// The camelCase key now resolves to the DATABASE_HOST env varconst host = Config.string("databaseHost")
Effect.runFork( host.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)))// => "localhost"ConfigProvider.nested
Section titled “ConfigProvider.nested”Prepends a prefix to every lookup, the provider-level counterpart to
Config.nested. Accepts a single string or a full Path. The prefix is
prepended after any mapInput transformation runs, so ordering matters when
composing with mapInput / constantCase.
import { Config, ConfigProvider } 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")
// Config.string("HOST").parse(scoped) // => "localhost"Installing as a Layer
Section titled “Installing as a Layer”For application-wide setup, install the provider with a Layer instead of
threading provideService through every call. Both layer functions accept
either a plain ConfigProvider or an Effect<ConfigProvider> (handy for
fromDotEnv / fromDir, which return Effects).
ConfigProvider.layer
Section titled “ConfigProvider.layer”Replaces the active provider entirely for all downstream effects.
import { Config, ConfigProvider, Effect } from "effect"
// Use a fixed JSON object as the config source for the whole appconst ConfigLayer = ConfigProvider.layer( ConfigProvider.fromUnknown({ port: 8080 }))
const program = Effect.gen(function* () { return yield* Config.number("port")})
// Effect.runSync(Effect.provide(program, ConfigLayer)) // => 8080ConfigProvider.layerAdd
Section titled “ConfigProvider.layerAdd”Augments the currently active provider rather than replacing it. By default the
new provider is a fallback (consulted only when the current provider returns
undefined); pass { asPrimary: true } to make the new provider win and fall
back to the existing one.
import { ConfigProvider } from "effect"
const defaults = ConfigProvider.fromUnknown({ HOST: "localhost", PORT: "3000" })
// The current env provider is tried first; `defaults` fills the gapsconst DefaultsLayer = ConfigProvider.layerAdd(defaults)
// Pass { asPrimary: true } to make `defaults` win and fall back to env insteadconst OverridesLayer = ConfigProvider.layerAdd(defaults, { asPrimary: true })Building nodes in a custom provider
Section titled “Building nodes in a custom provider”Inside a make lookup function you return Node values. Node is a
discriminated union of Value | Record | Array; Record and Array can carry
an optional co-located value (so the same path can be both a container and a
leaf, mirroring an env var like A=x that also has children A_FOO).
ConfigProvider.makeValue
Section titled “ConfigProvider.makeValue”Constructs a terminal string leaf node.
import { ConfigProvider } from "effect"
ConfigProvider.makeValue("3000")// => { _tag: "Value", value: "3000" }ConfigProvider.makeRecord
Section titled “ConfigProvider.makeRecord”Constructs an object-like container with a known set of child keys, plus an
optional co-located value.
import { ConfigProvider } from "effect"
ConfigProvider.makeRecord(new Set(["host", "port"]))// => { _tag: "Record", keys: Set(["host", "port"]), value: undefined }
ConfigProvider.makeRecord(new Set(["FOO"]), "x")// => { _tag: "Record", keys: Set(["FOO"]), value: "x" }ConfigProvider.makeArray
Section titled “ConfigProvider.makeArray”Constructs an indexed container with a known length, plus an optional
co-located value.
import { ConfigProvider } from "effect"
ConfigProvider.makeArray(3)// => { _tag: "Array", length: 3, value: undefined }A fuller custom provider, choosing the right node per path:
import { ConfigProvider, Effect } from "effect"
const store: Record<string, unknown> = { HOST: "localhost", TAGS: ["a", "b", "c"]}
const provider = ConfigProvider.make((path) => { const value = store[String(path[0])] if (value === undefined) return Effect.succeed(undefined) // not found if (typeof value === "string") { return Effect.succeed(ConfigProvider.makeValue(value)) } if (Array.isArray(value)) { return Effect.succeed(ConfigProvider.makeArray(value.length)) } return Effect.fail(new ConfigProvider.SourceError({ message: "unsupported" }))})Types and errors
Section titled “Types and errors”ConfigProvider.ConfigProvider (the interface and reference)
Section titled “ConfigProvider.ConfigProvider (the interface and reference)”The ConfigProvider interface exposes load(path) (resolves mapInput and
prefix transforms, then delegates to get), get(path) (raw store access
without transforms), and the stored mapInput / prefix. The
ConfigProvider.ConfigProvider value is the Context.Reference that holds the
active provider, defaulting to fromEnv().
import { ConfigProvider, Effect } from "effect"
const program = Effect.gen(function* () { // Retrieve the currently active provider const provider = yield* ConfigProvider.ConfigProvider return yield* provider.load(["HOST"]) // => { _tag: "Value", value: "localhost" } | undefined}).pipe( Effect.provideService( ConfigProvider.ConfigProvider, ConfigProvider.fromUnknown({ HOST: "localhost" }) ))ConfigProvider.Path
Section titled “ConfigProvider.Path”An ordered sequence of string | number segments addressing a node. String
segments name object keys; numeric segments index into arrays.
import type { ConfigProvider } from "effect"
const path: ConfigProvider.Path = ["database", "replicas", 0, "host"]ConfigProvider.SourceError
Section titled “ConfigProvider.SourceError”A Data.TaggedError ({ message: string; cause?: unknown }) signalling that a
backing store could not be read (I/O failure, permission error, etc.). Do not
use it for “key not found” — return undefined for that.
import { ConfigProvider, Effect } from "effect"
const failing = ConfigProvider.make(() => Effect.fail(new ConfigProvider.SourceError({ message: "connection refused" })))// failing.load(["X"]) fails with SourceError; orElse will NOT swallow itMocking config in tests
Section titled “Mocking config in tests”Because the provider is just a service, tests provide a deterministic one — no
need to mutate process.env.
import { Config, ConfigProvider, Effect } from "effect"
// The code under test reads its config the normal wayconst loadConfig = Config.all({ host: Config.string("HOST"), port: Config.port("PORT")})
// In a test, swap in a fixed providerconst testProvider = ConfigProvider.fromUnknown({ HOST: "localhost", PORT: "8080"})
const result = loadConfig.pipe( Effect.provideService(ConfigProvider.ConfigProvider, testProvider))// result yields { host: "localhost", port: 8080 } deterministicallyThis is the idiomatic way to test configuration-dependent code. See Testing for the broader pattern of providing test layers.