Redacted Secrets
Some configuration values — API keys, database passwords, tokens — must never
appear in logs, error messages, or serialized output. Redacted<A> wraps a
sensitive value so that any normal rendering path (string interpolation,
JSON.stringify, console.log, inspection) shows <redacted> instead of the
real value. The underlying value stays recoverable, but only through an explicit,
visible call.
import { Config, Effect, Redacted } from "effect"
const program = Effect.gen(function* () { // Config.redacted parses the value and wraps it in Redacted<string> const apiKey = yield* Config.redacted("API_KEY")
// Logging the wrapper is safe — it renders as <redacted> yield* Effect.log(`Loaded API key: ${apiKey}`)
// Unwrap only at the trusted boundary where the secret is actually used return yield* callApi(Redacted.value(apiKey))})
declare const callApi: (key: string) => Effect.Effect<unknown>
// API_KEY=sk-1234567890 node app.js// Logs: Loaded API key: <redacted>The key idea: a Redacted is safe to pass around, log, and store. The secret is
exposed only at the moment you call Redacted.value, which makes those call
sites easy to audit.
Reading secrets from config
Section titled “Reading secrets from config”Config.redacted(name) is the usual entry point. It reads a string from the
provider, validates it, and returns a
Redacted<string>:
import { Config, Context, Effect, Layer, Redacted } from "effect"
// A service that needs a secret token to do its jobclass GitHubClient extends Context.Service<GitHubClient, { readonly listRepos: Effect.Effect<ReadonlyArray<string>>}>()("app/GitHubClient") { static readonly layer = Layer.effect( GitHubClient, Effect.gen(function* () { // Read the secret once, when the layer is built const token = yield* Config.redacted("GITHUB_TOKEN")
return { listRepos: Effect.gen(function* () { // The unwrapped value never leaves this Effect const auth = `Bearer ${Redacted.value(token)}` yield* Effect.log("Fetching repos") // token is not logged return [] }) } }) )}This is the idiomatic shape: read the secret into a service when its layer is
built, keep it as a Redacted inside the service, and unwrap it only where it is
sent to the external system.
Creating and inspecting Redacted values
Section titled “Creating and inspecting Redacted values”Outside of config, build a Redacted directly with Redacted.make. An optional
label is shown in the rendered placeholder, which helps you tell secrets apart
in logs without revealing them.
import { Redacted } from "effect"
const token = Redacted.make("secret-token", { label: "github-token" })
String(token) // "<redacted:github-token>"JSON.stringify(token) // "\"<redacted:github-token>\""Redacted.value(token) // "secret-token" (explicit, auditable)Comparing redacted values
Section titled “Comparing redacted values”Equality and hashing operate on the underlying value, so two Redacteds wrapping
the same secret are equal — without ever exposing it. For custom element types,
Redacted.makeEquivalence derives an equivalence from one on the inner value:
import { Equivalence, Redacted } from "effect"
const a = Redacted.make("1234567890")const b = Redacted.make("1234567890")const c = Redacted.make("0000000000")
const eq = Redacted.makeEquivalence(Equivalence.strictEqual<string>())
eq(a, b) // true — same secreteq(a, c) // false — different secretWiping a secret
Section titled “Wiping a secret”When a secret is no longer needed, Redacted.wipeUnsafe removes it from the
internal registry so future Redacted.value calls on that wrapper fail. This is
a best-effort scrub for long-lived processes, not a security guarantee.
import { Redacted } from "effect"
const token = Redacted.make("one-time-token")
Redacted.value(token) // "one-time-token"
Redacted.wipeUnsafe(token)
// Redacted.value(token) now throws: "Unable to get redacted value"