Configuration

Effect.Config + Layer — validated at startup, accessed via context

The Pattern

Configuration is a Layer. Define it with Effect.Config, expose it via Context.Tag, and yield from context in handlers. No global config object.

Defining Config

import { Context, Config, Effect, Layer } from "effect"

// 1) Define the config shape
const AppConfigSchema = Config.all({
  DATABASE_URL: Config.string("DATABASE_URL"),
  REDIS_URL: Config.string("REDIS_URL"),
  PORT: Config.integer("PORT").pipe(Config.withDefault(3000)),
  NODE_ENV: Config.literal("development", "production", "test")("NODE_ENV").pipe(
    Config.withDefault("development")
  )
})

// 2) Infer the type
type AppConfig = Config.Config.Success<typeof AppConfigSchema>

// 3) Create the Context.Tag
class Config extends Context.Tag("Config")<Config, AppConfig>() {}

// 4) Create the Layer — reads from environment
const ConfigLive = Layer.effect(
  Config,
  Effect.config(AppConfigSchema)
)

Using in Layers

// Other layers can depend on Config
const PgPoolLive = Layer.scoped(
  PgPool,
  Effect.gen(function* () {
    const cfg = yield* Config
    const pool = new Pool({ connectionString: cfg.DATABASE_URL })
    yield* Effect.addFinalizer(() =>
      Effect.tryPromise(() => pool.end()).pipe(Effect.orDie)
    )
    return pool
  })
).pipe(Layer.provide(ConfigLive))

Using in Handlers

HttpRouter.get("/info", Effect.gen(function* () {
  const cfg = yield* Config
  return HttpServerResponse.json({
    environment: cfg.NODE_ENV,
    port: cfg.PORT
  })
}))

Nested Config

// Group related config with Config.nested
const DatabaseConfig = Config.all({
  url: Config.string("URL"),
  poolMin: Config.integer("POOL_MIN").pipe(Config.withDefault(2)),
  poolMax: Config.integer("POOL_MAX").pipe(Config.withDefault(10)),
  ssl: Config.boolean("SSL").pipe(Config.withDefault(false))
}).pipe(Config.nested("DATABASE"))

// Reads: DATABASE_URL, DATABASE_POOL_MIN, DATABASE_POOL_MAX, DATABASE_SSL

Secret Values

// Config.secret wraps the value — won't leak in logs
const SecureConfig = Config.all({
  apiKey: Config.secret("API_KEY"),
  dbPassword: Config.secret("DB_PASSWORD")
})

// Access the underlying value
const cfg = yield* Effect.config(SecureConfig)
const key = Secret.value(cfg.apiKey) // string

Validation at Startup

Config is validated when the Layer is built. Missing or invalid values fail fast with clear error messages:

ConfigError: Missing data at NODE_ENV: Expected one of
("development" | "production" | "test") but received "staging"

Testing with Config

// Override config in tests
const ConfigTest = Layer.succeed(Config, {
  DATABASE_URL: "postgres://localhost/test",
  REDIS_URL: "redis://localhost",
  PORT: 3001,
  NODE_ENV: "test" as const
})

await Effect.runPromise(
  myEffect.pipe(Effect.provide(ConfigTest))
)