# Config Providers

A `ConfigProvider` is the backing data source that a [`Config`](https://effect.plants.sh/configuration/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.

```ts
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 environment
const 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

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 of `string | number` segments addressing a node. String
  segments name object keys; numbers index into arrays.
- **`load(path)`** — resolves a path to `Effect<Node | undefined, SourceError>`.
  `undefined` means *not found*; `SourceError` means the source itself failed.

## 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`

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`.

```ts
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))
```
**Note:** Because of `_` splitting, querying a parent path like `["DATABASE"]` returns a
  `Record` node with child key `"HOST"`, even if no env var literally named
  `DATABASE` exists. If every immediate child name is numeric, the node is
  reported as an `Array` instead.

### `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`.

```ts
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 array
const replica = Config.string("host").parse(
  provider.pipe(ConfigProvider.nested(["replicas", 1]))
)
// Effect.runSync(replica) // => "r2"
```

### `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 }`.

```ts
import { Config, ConfigProvider } from "effect"

const provider = ConfigProvider.fromDotEnvContents(`
export HOST=localhost
PORT=3000
# this is a comment
GREETING="hello\\nworld"
`)

// Config.string("HOST").parse(provider) // => "localhost"
```

```ts
import { ConfigProvider } from "effect"

// expandVariables resolves ${...} references and ${VAR:-default} fallbacks
const 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`

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.

```ts
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>
```
**Note:** Provide a platform `FileSystem` layer (for example from
  `@effect/platform-node`) when running an Effect that uses `fromDotEnv` or
  `fromDir`.

### `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 `"/"`.

```ts
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`

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).

```ts
import { ConfigProvider, Effect } from "effect"

// Back a provider with an arbitrary in-memory store
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
  )
})
// Config.string("host").parse(provider) // => "localhost"
```

## 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`

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.

```ts
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`

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`.

```ts
import { ConfigProvider } from "effect"

const provider = ConfigProvider.fromEnv({ env: { APP_HOST: "localhost" } })

// Uppercase string segments before lookup
const upper = ConfigProvider.mapInput(provider, (path) =>
  path.map((seg) => (typeof seg === "string" ? seg.toUpperCase() : seg))
)
// path ["app_host"] now resolves to env var APP_HOST
```

### `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.

```ts
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 var
const host = Config.string("databaseHost")

Effect.runFork(
  host.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider))
)
// => "localhost"
```

### `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`.

```ts
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

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`

Replaces the active provider entirely for all downstream effects.

```ts
import { Config, ConfigProvider, Effect } from "effect"

// Use a fixed JSON object as the config source for the whole app
const ConfigLayer = ConfigProvider.layer(
  ConfigProvider.fromUnknown({ port: 8080 })
)

const program = Effect.gen(function* () {
  return yield* Config.number("port")
})

// Effect.runSync(Effect.provide(program, ConfigLayer)) // => 8080
```

### `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.

```ts
import { ConfigProvider } from "effect"

const defaults = ConfigProvider.fromUnknown({ HOST: "localhost", PORT: "3000" })

// The current env provider is tried first; `defaults` fills the gaps
const DefaultsLayer = ConfigProvider.layerAdd(defaults)

// Pass { asPrimary: true } to make `defaults` win and fall back to env instead
const OverridesLayer = ConfigProvider.layerAdd(defaults, { asPrimary: true })
```

## 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`

Constructs a terminal string leaf node.

```ts
import { ConfigProvider } from "effect"

ConfigProvider.makeValue("3000")
// => { _tag: "Value", value: "3000" }
```

### `ConfigProvider.makeRecord`

Constructs an object-like container with a known set of child keys, plus an
optional co-located `value`.

```ts
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`

Constructs an indexed container with a known `length`, plus an optional
co-located `value`.

```ts
import { ConfigProvider } from "effect"

ConfigProvider.makeArray(3)
// => { _tag: "Array", length: 3, value: undefined }
```

A fuller custom provider, choosing the right node per path:

```ts
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

### `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()`.

```ts
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`

An ordered sequence of `string | number` segments addressing a node. String
segments name object keys; numeric segments index into arrays.

```ts
import type { ConfigProvider } from "effect"

const path: ConfigProvider.Path = ["database", "replicas", 0, "host"]
```

### `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.

```ts
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 it
```

## Mocking config in tests

Because the provider is just a service, tests provide a deterministic one — no
need to mutate `process.env`.

```ts
import { Config, ConfigProvider, Effect } from "effect"

// The code under test reads its config the normal way
const loadConfig = Config.all({
  host: Config.string("HOST"),
  port: Config.port("PORT")
})

// In a test, swap in a fixed provider
const testProvider = ConfigProvider.fromUnknown({
  HOST: "localhost",
  PORT: "8080"
})

const result = loadConfig.pipe(
  Effect.provideService(ConfigProvider.ConfigProvider, testProvider)
)
// result yields { host: "localhost", port: 8080 } deterministically
```

This is the idiomatic way to test configuration-dependent code. See
[Testing](https://effect.plants.sh/testing/) for the broader pattern of providing test layers.