@toggld/node
v0.3.0
Published
Toggld SDK for Node.js — local flag evaluation with a background definition sync. Plain Promise API by default; native Effect API via @toggld/node/effect.
Downloads
17
Maintainers
Readme
@toggld/node
The Toggld SDK for Node.js. Evaluate feature flags from your backend against the Toggld flag API.
Two API surfaces ship from one implementation:
@toggld/node— a plain Promise API. No Effect types in any signature.@toggld/node/effect— the native Effect API: the client as a service +Layer.
Two evaluation modes:
- Remote (default) — every call hits the flag API, which evaluates the flag against your context and returns the resolved value (OpenFeature "remote evaluation" over the OFREP endpoints).
- Local (
mode: "local") — recommended for long-lived servers. Flag definitions sync in the background (a snapshot fetch plus a live WebSocket stream, with polling fallback) and every evaluation runs in-process on the bundled engine: microsecond latency, zero server requests per evaluation, and changes pushed to your process within milliseconds. Requires a secret SDK key (ffk_…) — public browser tokens cannot read rulesets. See ADR-0012. Local mode can also read its snapshot from a file instead of the network — see Local flag files below.
Install
pnpm add @toggld/nodePromise API
import { ToggldClient } from "@toggld/node"
const client = await ToggldClient.create({
sdkKey: process.env.TOGGLD_SDK_KEY!, // an evaluation API key
endpoint: "https://your-flag-api.example.com",
mode: "local", // omit for remote evaluation
})
// Typed getters — return the default on any error, never throw.
const on = await client.getBooleanValue("new-checkout", false, {
targetingKey: "user-123",
plan: "premium",
})
// Full OpenFeature evaluation details.
const details = await client.getStringDetails("theme", "light", { targetingKey: "user-123" })
// ^ { flagKey, value, variant, reason, errorCode, errorMessage, flagMetadata }
// All flags for the environment in one request.
const everything = await client.evaluateAll({ targetingKey: "user-123" })
await client.close() // releases the underlying runtimeGetters exist for each flag type: getBooleanValue / getBooleanDetails,
getStringValue / …, getNumberValue / …, and getObjectValue<T> / ….
Effect API
import { Effect } from "effect"
import { ToggldClient, layer } from "@toggld/node/effect"
const program = Effect.gen(function* () {
const flags = yield* ToggldClient
return yield* flags.getBooleanValue("new-checkout", false, { targetingKey: "user-123" })
})
program.pipe(
Effect.provide(layer({ sdkKey, endpoint })),
Effect.runPromise,
)The layer is self-contained — it wires its own HTTP backend, so you only
provide this one layer. Lifecycle (the HTTP client) is managed by the enclosing
scope.
Options
| Option | Type | Default | Notes |
| ---------------- | --------------------- | ---------------- | ---------------------------------------------------------------- |
| sdkKey | string | — | Evaluation (or management) API key; sent as Bearer. |
| endpoint | string | — | Base URL of the flag API, no trailing path. |
| mode | "remote" \| "local" | "remote" | Local = background definition sync + in-process engine. |
| sync.transport | "ws" \| "poll" | "ws" | Live stream (with poll safety net) vs polling only. |
| sync.pollIntervalMs | number | 30000 | Snapshot poll cadence; cold start retries faster. |
| sync.readyTimeoutMs | number | 5000 | How long create waits for the first snapshot. |
| sync.file | string \| boolean | — | Read the snapshot from a local file (offline); true ⇒ ./toggld.flags.json. |
| defaultContext | EvaluationContext | {} | Merged under per-call context (per-call attributes win). |
| timeoutMs | number | 5000 | Per-request timeout. |
| fetch | typeof fetch | globalThis.fetch | Custom fetch (tests, proxies, instrumentation). |
In local mode create resolves once the first snapshot has synced (or after
sync.readyTimeoutMs); until a snapshot arrives, evaluations return your
defaults with errorCode: "PROVIDER_NOT_READY" and sync keeps retrying.
client.ready() awaits sync explicitly. Updates arrive as WebSocket pushes
within milliseconds of a management change; on runtimes where the stream is
unavailable the client polls instead, automatically.
How fast is local mode?
pnpm bench:sync (from the repo root) runs the same workload with sync off vs
on — representative numbers on a laptop, loopback server:
sync OFF (remote OFREP) ~11,600 evals/s p50 2.4 ms
sync ON (local engine) ~1,090,000 evals/s p50 27 µs (0.6 µs single-caller)Local flag files (offline)
For local development, CI, or offline runs, a client can evaluate against a file instead of syncing over the network — same engine, same results, no flag API required. Generate the file with the bundled CLI:
# Fetch the environment's current flags into ./toggld.flags.json.
# Prompts for the key if you omit --sdk-key (or set TOGGLD_SDK_KEY).
npx @toggld/node pull --sdk-key ffk_… --api https://your-flag-api.example.comThen point a client at it — no sdkKey or endpoint needed:
const client = await ToggldClient.create({
mode: "local",
sync: { file: "./toggld.flags.json" }, // or `file: true` for that default path
})The file is a FlagSnapshot (the exact wire snapshot the SDK syncs) wrapped in a
small provenance envelope, so it is human-readable, diffable, and editable —
flip an enabled, nudge a rollout, commit a fixture. Edits are picked up
live (the file is watched); no restart.
You can also redirect any client to a file with no code change by setting
TOGGLD_FLAGS_FILE — it overrides the network source even under the default
remote mode:
TOGGLD_FLAGS_FILE=./toggld.flags.json node server.jspull needs a secret key (ffk_…); it is one-shot and idempotent (an unchanged
version is not rewritten — pass --force to override). See
ADR-0013. Remote-segment
membership (ADR-0006) is not yet available offline; a file-backed client warns
once and treats a remote segment's non-rule-matched subjects as non-members.
Error semantics
Evaluation never throws. On a network failure, timeout, unknown flag, server
error, or a value whose type doesn't match the requested getter, the call
resolves to your default value. The *Details methods carry reason:
"ERROR" and an OpenFeature errorCode (FLAG_NOT_FOUND, TYPE_MISMATCH,
TARGETING_KEY_MISSING, PARSE_ERROR, GENERAL, …) describing what happened.
