Hyperlinkv0.8.0-beta.28
DraftMigration checklist — 1/6
  • Current API — no legacy surface
  • LSP code previews
  • Clean example types
  • Verified examples
  • Follows the docs standards
  • Owner-reviewed

Configuration

How an application declares configuration, and how the parts that change while it runs are swapped safely. This builds on Effects Config — it doesnt replace it, it adds a hot-swap layer on top.

Hot-swappable config

DynamicConfig wraps the Config fields that may change at runtime. Reads keep the native Config interfaceyield* cfg.apiKey reads through the ambient ConfigProvider exactly like any Config, with R = never. A swap writes a raw override into a store, so the next read of that key — through the wrapper or a plain Config — sees the new value.

import { Config, Effect } from "effect"
import * as DynamicConfig from "hyperlink-ts/DynamicConfig"

const cfg = DynamicConfig.make({
  baseUrl: Config.string("BASE_URL").pipe(Config.withDefault("https://api.example.com")),
  apiKey: DynamicConfig.swappable(Config.redacted("API_KEY")),
})

const program = Effect.gen(function* () {
  const url = yield* cfg.baseUrl     // read — a native Config
  yield* cfg.apiKey.set("rotated")   // swap — the control lives on the field
  yield* cfg.apiKey.reset            // revert to env / default
})

Provide the store once with layer, at or above any scope that reads or swaps:

program.pipe(Effect.provide(DynamicConfig.layer(cfg)))

Scoped vs. process-wide

A field is made swappable one of two ways, and the choice is about who may swap it by string (the setByKey path, below):

  • swappablescoped. No global footprint. Its typed control (.set / .reset / .changes) works wherever layer is provided; the string path opens for it only in a provision you hand its config to, layer(cfg).

  • globalSwappableprocess-wide. Declared anywhere (even at module scope), with no wiring, swappable by string on any runtime. Reach for it when a remote/RPC handler does the swapping.

// scoped — reachable by setByKey only where layer(cfg) is provided
const cfg = DynamicConfig.make({ token: DynamicConfig.swappable(Config.string("TOKEN")) })

// process-wide — setByKey works on any runtime, no wiring
const apiKey = DynamicConfig.globalSwappable(Config.redacted("API_KEY"))

Swapping by key

setByKey swaps a raw env key — the path for an RPC handler that doesnt hold a field. It is gated by the allowlist (a runtimes globalSwappable keys, plus any swappable keys in the config given to its layer) and validates the value before writing it.

// inside an RPC procedure on the target runtime:
yield* DynamicConfig.setByKey("API_KEY", "rotated")
// → fails with ConfigKeyNotSwappable if that key was not declared swappable here

Composing and freezing

const whole = yield* DynamicConfig.all(cfg)  // read the whole bag at once, like Config.all
const child = DynamicConfig.extend(cfg, {    // clone a bag and add fields
  retries: Config.int("RETRIES").pipe(Config.withDefault(3)),
})
const readonly = DynamicConfig.freeze(cfg)   // .set won't compile against a frozen view

Hot-swappable config is the first piece of this page — more configuration guidance lands here as the Resources chapters firm up.

Edit this page on GitHub