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.
Built-in providers
Section titled “Built-in providers”| 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 |
Environment variables
Section titled “Environment variables”fromEnv joins path segments with _ for lookups, and also splits env var
names on _ to discover nested keys. So DATABASE_HOST=localhost is reachable
both at the flat path ["DATABASE_HOST"] and at the nested path
["DATABASE", "HOST"].
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}`)}).pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider))JSON / plain objects
Section titled “JSON / plain objects”fromUnknown is backed by any JavaScript value, typically a parsed JSON object.
String keys index into objects and numeric keys index into arrays, so it handles
arbitrarily nested config naturally.
import { Config, ConfigProvider, Effect } from "effect"
const provider = ConfigProvider.fromUnknown({ server: { host: "localhost", port: 8080 }, replicas: [{ host: "r1" }, { host: "r2" }]})
const program = Effect.gen(function* () { const server = yield* Config.all({ host: Config.string("host"), port: Config.port("port") }).pipe(Config.nested("server"))
yield* Effect.log(`${server.host}:${server.port}`)}).pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)).env files
Section titled “.env files”fromDotEnv reads and parses a .env file from disk. It needs a FileSystem
in context (from your platform layer), and returns an Effect that produces the
provider. fromDotEnvContents does the same from a string you already have.
import { ConfigProvider, Effect } from "effect"
const program = Effect.gen(function* () { // Reads ".env" by default; pass { path } to override const provider = yield* ConfigProvider.fromDotEnv()
return yield* myApp.pipe( Effect.provideService(ConfigProvider.ConfigProvider, provider) )})
declare const myApp: Effect.Effect<void>Directory of files
Section titled “Directory of files”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.
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) )})
declare const myApp: Effect.Effect<void>Installing a provider as a Layer
Section titled “Installing a provider as a Layer”For application-wide setup, install the provider with a Layer instead of
threading provideService through every call. ConfigProvider.layer replaces
the active provider entirely.
import { ConfigProvider, Layer } from "effect"
// Use a fixed JSON object as the config source for the whole appconst ConfigLayer = ConfigProvider.layer( ConfigProvider.fromUnknown({ port: 8080 }))
// Provide it alongside your other layers: Layer.provide(appLayer, ConfigLayer)Use ConfigProvider.layerAdd when you want to augment the existing provider
rather than replace it — for example, layering defaults underneath the real
environment:
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 insteadComposing and transforming providers
Section titled “Composing and transforming providers”Providers are values you can combine and reshape.
Fallback with orElse — try one source, then another when a path is not
found:
import { 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)Re-casing keys with constantCase — bridge 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)))ConfigProvider.constantCase is a preset of the more general
ConfigProvider.mapInput, which lets you rewrite path segments however you like.
ConfigProvider.nested(provider, prefix) prepends a prefix to every lookup, the
provider-level counterpart to Config.nested.
Mocking 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.
Writing a custom provider
Section titled “Writing a custom provider”ConfigProvider.make builds a provider from a lookup function. Return a Node
for a found path, undefined for “not found”, and fail with SourceError only
for genuine I/O errors.
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 )})Use ConfigProvider.makeValue, makeRecord, and makeArray to describe leaves,
objects, and arrays respectively. This is how you would back configuration with a
database, a secrets manager, or a remote config service.