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"Redacted in schemas and config
Section titled “Redacted in schemas and config”Config.redacted(name) is built on Schema.Redacted(Schema.String) — it parses
a string from the active provider and wraps it in a Redacted<string> in one
step. Use the schema directly when a secret is a field of a larger decoded
structure (a request body, a config object) rather than a standalone config key.
import { Config, ConfigProvider, Effect, Redacted } from "effect"
const program = Effect.gen(function* () { const token = yield* Config.redacted("API_TOKEN") return Redacted.value(token) // unwrap at the trusted boundary})
const provider = ConfigProvider.fromUnknown({ API_TOKEN: "sk-secret" })
program.pipe(Effect.provide(ConfigProvider.layer(provider)))See Config for the full set of config constructors and
Schema for using Schema.Redacted inside larger decoded shapes.
Redacted API reference
Section titled “Redacted API reference”The Redacted module is small — a constructor, an unwrapper, a guard, a wipe,
and an equivalence builder — plus the rendering and equality behavior baked into
every wrapper. This section enumerates every public export.
Redacted.make
Section titled “Redacted.make”Wraps a sensitive value, returning a Redacted<A>. The optional label is shown
inside the rendered placeholder so you can tell wrappers apart in logs. The type
parameter defaults to string.
import { Redacted } from "effect"
const plain = Redacted.make("sk-1234567890")String(plain) // => "<redacted>"
const labeled = Redacted.make("sk-1234567890", { label: "api-key" })String(labeled) // => "<redacted:api-key>"
// Works with any value type, not just stringsconst creds = Redacted.make({ user: "admin", pass: "hunter2" })String(creds) // => "<redacted>"Redacted.value
Section titled “Redacted.value”Unwraps a Redacted, returning the underlying value. This is the only way to
read the secret, so call sites are easy to audit. Throws
Error("Unable to get redacted value") if the wrapper has been wiped.
import { Redacted } from "effect"
const token = Redacted.make("secret-token")
Redacted.value(token) // => "secret-token"Redacted.isRedacted
Section titled “Redacted.isRedacted”A type guard that returns true for any Redacted wrapper. When it returns
true, TypeScript narrows the value to Redacted<unknown>.
import { Redacted } from "effect"
const secret = Redacted.make("my-secret")
Redacted.isRedacted(secret) // => trueRedacted.isRedacted("plain") // => falseRedacted.isRedacted(null) // => falseRedacted.wipeUnsafe
Section titled “Redacted.wipeUnsafe”Deletes the wrapper’s stored value from the internal redacted registry, so future
Redacted.value calls on that wrapper throw. Returns true if a value was
present and removed, false otherwise. This does not zero memory and does not
affect other references to the original value — it is a best-effort scrub, not a
cryptographic guarantee.
import { Redacted } from "effect"
const token = Redacted.make("one-time-token")
Redacted.value(token) // => "one-time-token"
Redacted.wipeUnsafe(token) // => true (value was removed)Redacted.wipeUnsafe(token) // => false (nothing left to remove)
// Redacted.value(token) now throws: "Unable to get redacted value"Redacted.makeEquivalence
Section titled “Redacted.makeEquivalence”Derives an Equivalence<Redacted<A>> from an Equivalence<A>, comparing the
underlying values without exposing them. Useful when an API needs to compare
secrets structurally (e.g. a non-string payload).
import { Equivalence, Redacted } from "effect"
const eq = Redacted.makeEquivalence( Equivalence.mapInput( Equivalence.String, (c: { token: string }) => c.token ))
const a = Redacted.make({ token: "abc" })const b = Redacted.make({ token: "abc" })const c = Redacted.make({ token: "xyz" })
eq(a, b) // => true — same token, never exposedeq(a, c) // => falseRender behavior: toString / toJSON
Section titled “Render behavior: toString / toJSON”Every wrapper’s toString() and toJSON() return the same <redacted> or
<redacted:label> placeholder. This is what makes string interpolation,
JSON.stringify, console.log, and Node inspection all safe by default.
import { Redacted } from "effect"
const token = Redacted.make("secret", { label: "session" })
`${token}` // => "<redacted:session>"JSON.stringify({ token }) // => '{"token":"<redacted:session>"}'Equality and hashing
Section titled “Equality and hashing”Redacted implements the Equal.Equal interface, so structural equality and
hashing operate on the underlying value — two wrappers holding the same secret
are equal, without exposing it. The wrapper is also Pipeable.
import { Equal, Redacted } from "effect"
const a = Redacted.make("same")const b = Redacted.make("same")const c = Redacted.make("different")
Equal.equals(a, b) // => trueEqual.equals(a, c) // => falseRedacted<A = string> is the wrapper interface (it carries the optional label
and extends Equal.Equal and Pipeable). The Redacted namespace holds the
type-level helpers:
Redacted.Redacted.Value<T>— extracts the underlying value type from aRedactedtype.Redacted.Redacted.Variance<A>— the internal covariance marker; you rarely reference it directly.
import { Redacted } from "effect"
type ApiKey = Redacted.Redacted<{ readonly token: string }>type ApiKeyValue = Redacted.Redacted.Value<ApiKey>// => { readonly token: string }
const rotate = (value: ApiKeyValue): ApiKeyValue => ({ token: `${value.token}:rotated`})
rotate({ token: "secret" }) // => { token: "secret:rotated" }