SDKs
JavaScript SDK
On this page
Overview
npm install @nanoflags/sdk| Import | Use |
|---|---|
@nanoflags/sdk | Node, browsers and edge runtimes: createClient, types. |
@nanoflags/sdk/react | React bindings: NanoFlagsProvider, useFlag, useFlagsStatus. |
@nanoflags/sdk/node | Test-session propagation with AsyncLocalStorage. |
ESM with TypeScript types; the only runtime dependency is the NanoFlags evaluation engine. React is an optional peer dependency (18 or newer).
createClient options
import { createClient } from "@nanoflags/sdk";
const flags = createClient({
sdkKey: process.env.NANOFLAGS_SDK_KEY!,
baseUrl: "https://edge.nanoflags.io",
service: "checkout-api",
pollIntervalMs: 30_000,
streaming: true,
live: true,
events: { sampleRate: 0.1 },
});| Option | Default | Description |
|---|---|---|
sdkKey | required | nf_srv_… on servers, nf_pub_… in browsers. Never ship a server key to a browser. |
baseUrl | required | Edge URL, https://edge.nanoflags.io. |
service | "unknown" | Name this process reports under ("which revision is this service running"). |
pollIntervalMs | 30000 | Conditional poll interval (± 20 % jitter). 0 disables polling, including the safety poll. |
streaming | true | Receive updates without waiting for a poll (live socket, else SSE). false = polling only. |
live | true | false skips the WebSocket and uses SSE + polling. |
livePollIntervalMs | 300000 | Safety poll interval while the live socket is healthy. |
webSocket | global WebSocket | WebSocket constructor, e.g. require("ws") on Node older than 22. Without one the SDK uses SSE + polling. |
bootstrap | none | A bundle obtained elsewhere (embedded at build time, or passed from your server for SSR). Trusted as given. |
store | IndexedDB in browsers, none elsewhere | Any async string store { get(), set(v) }; false disables persistence. |
events | {} | enabled (true), sampleRate (0.1), flushIntervalMs (10000), reportIntervalMs (60000), redact(ctx). false disables telemetry. See Telemetry. |
verify | on for https://edge.nanoflags.io (from 0.4.0), off elsewhere | { publicKeys: JsonWebKey[] | "fetch", require?: boolean }, or false to turn verification off. See Signature verification. |
fetch | global fetch | Custom fetch implementation. |
Methods and properties
| Member | Description |
|---|---|
ready(): Promise<void> | Resolves when a usable bundle is loaded (network, store or bootstrap), or when the first load attempt finished without one. Never rejects: check status. |
boolean · string · number · json (key, fallback, ctx?) | The flag's value, or fallback. Synchronous. |
evaluate(key, fallback, ctx?) | The full result: value, reason, ruleIndex, ruleId, inRollout, bucket, revision, trace. No type check. |
status | "not-ready" · "ready" · "stale", see Status. |
revision | Revision of the bundle in use; null without one. |
transport | How updates reach the client now: "live" (WebSocket open), "stream" (SSE), "poll". |
on("update" | "error", cb) | Subscribe; returns an unsubscribe function. |
session(token, { service? }) | Evaluator for a test session. Never rejects. |
useSession(token | null) · sessionHeaders() | Browser only: join or leave a test session for this tab; headers for downstream calls. |
refresh() | Revalidate now. Resolves when finished, successfully or not. |
flush() | Send queued telemetry now. |
close() | Stop timers, the live connection and telemetry. Evaluations keep working on the last bundle. |
The context is a plain object of JSON values. key is the stable identity used for rollout bucketing.
Status
status describes the bundle, not individual flags:
not-ready: no bundle. Every accessor returns its fallback.ready: the bundle was confirmed by the edge on the last attempt (or supplied asbootstrapand no attempt has failed yet).stale: a bundle is in use but is not known to be current: restored from the store and not yet revalidated, the last revalidation failed (offline, edge error, rejected key), or the edge served its last-known-good copy. Evaluations continue on that bundle while the SDK retries (after a failure: 1 s, 2 s, 4 s … up to the poll interval).
The SDK never discards a working bundle because of an error and never replaces a bundle with an older one. If a key is revoked, the client emits error (unauthorized) and keeps evaluating the last bundle as stale: revocation stops updates, it does not erase configuration that already reached a process.
Events
update
flags.on("update", (e) => {
e.revision; // new revision
e.previousRevision; // number | null
e.checksum; // string | null
e.source; // "network" | "store" | "bootstrap"
e.transport; // "live" (pushed) | "stream" (fetched after an SSE notification) | "poll" (everything else)
e.latencyMs; // commit → this event, only for pushed revisions with a commit time; otherwise null
});latencyMs compares the database clock with the local clock, so it is only as accurate as the two clocks agree. Current measured propagation is on the status page.
error
Receives a NanoFlagsError with a code (and httpStatus where relevant). Errors never interrupt evaluation.
| code | Meaning |
|---|---|
network | The edge could not be reached. |
unauthorized | The key was rejected (unknown or revoked, HTTP 401/403). |
http | Another unexpected HTTP status. |
invalid_bundle | The response was not a usable bundle. |
store | Reading or writing the persistence store failed. |
config | Invalid options, for example a private JWK passed to verify. |
signature | A bundle failed signature verification and was not used. |
session | A test-session lookup failed. |
engine | A bundle needs a newer evaluation engine than this SDK has. It was not used; upgrade the SDK. |
environment_deleted | The environment of this key was deleted. See Deleted environments. |
Fallbacks
The fallback argument is returned when NanoFlags cannot answer:
| Situation | reason |
|---|---|
No bundle loaded yet (not-ready) | flag_not_found |
| The flag is not in the bundle (unknown, archived, or not client-exposed with a client key) | flag_not_found |
The flag's value has another type than the accessor (boolean() on a string flag) | error |
| The configuration is malformed | error |
A disabled flag is not a fallback case: it returns the flag's configured off value (reason off).
Transports
- Live push (default): one WebSocket to
GET /v1/live. When a revision is published, the edge sends arevmessage carrying the signed bundle itself, so it is applied without another request; on (re)connect ahellomessage carries the current revision. The key travels in the WebSocket subprotocol list, never in the URL. The SDK pings about every 30 s, replaces the socket about every 50 minutes (so the key is validated again), and reconnects with jittered exponential backoff (1 s, 2 s, 4 s … capped at 5 minutes, each × 0.5–1.5). While the socket is healthy, SSE is not opened and polling drops tolivePollIntervalMs. - Stream (fallback): when the socket cannot be established (no
WebSocketin the runtime, a proxy that blocks upgrades, or the edge answering503 live_unavailable), the SDK opens a server-sent-events stream carrying only{ revision, checksum }and fetches the bundle conditionally. Client keys useEventSourcewith?key=; everything else a fetch stream with theAuthorizationheader. The SDK keeps retrying the live socket and closes the stream once a socket opens. - Polling (safety net): a conditional
GET /v1/bundleeverypollIntervalMs± 20 %. Hidden browser tabs do not poll and catch up when they become visible.
live: false keeps stream + polling; streaming: false leaves polling only. A new bundle replaces the old one in a single assignment, so an evaluation sees one revision in full, never a mixture. The SDK never moves to an older revision than one it was pushed.
Signature verification
The SDK checks the Ed25519 signature before a new bundle replaces the one in use, for fetched and pushed bundles alike.
- Default from 0.4.0: with
baseUrlset tohttps://edge.nanoflags.io, the SDK verifies every bundle against the NanoFlags signing keys it ships with (and loadsGET /v1/keysonce when a bundle names a key id it does not know) and rejects unsigned bundles. With any otherbaseUrl, nothing is verified unless you configureverify. GET /v1/keysreturns a JWKS-style list of public keys, each with akidand astatus:active(signs new bundles) orretiring(still valid for bundles signed before a rotation). See Signatures.- Verification needs Ed25519 in WebCrypto: Node.js 20+, current browsers, Cloudflare Workers, Deno, Bun. On a runtime without it, set
verify: false; otherwise signed bundles are rejected because they cannot be checked.
To pin your own copy of the keys (for example, to verify bundles fetched through your own proxy or cache):
const flags = createClient({
sdkKey, baseUrl,
verify: {
publicKeys: [{ kty: "OKP", crv: "Ed25519", x: "wQu1EsaE-yTjqpUbibGyBrajy3IiRb7LyaiqeYRxG4U", kid: "k1" }],
require: true,
},
});- Bad signature: the update is rejected, the previous bundle stays in use,
statusdoes not change, anderrorfires withsignature. A pushed bundle that does not verify also makes the SDK ask the edge over HTTP what is current. - Unsigned bundles (
X-NanoFlags-Signed: false, served briefly while signing catches up after a publish) are refused with the default settings on the NanoFlags edge; the SDK fetches again shortly. With a customverifythey are accepted unlessrequire: true. Withoutrequire, someone who can alter responses can also strip the signature. publicKeys: "fetch"loads keys once fromGET /v1/keys(reloaded at most once a minute when a bundle names an unknown key id). This detects corruption and tampering in storage and caches, but the keys are only as trustworthy as the edge that served them. Pin the keys in your build if the edge is in your threat model.- After the first verified bundle the client remembers the environment id and rejects bundles signed for another environment.
- Only public keys are accepted; a JWK with a private part (
d) is ignored and reported as aconfigerror. - Not covered: test-session bundles are not signed, and a signature says nothing about freshness; it does not stop someone from withholding updates.
Engine version
Every bundle names the evaluation engine version it was written for (engine). An SDK refuses a bundle from a newer engine than it supports: it keeps its previous configuration, does not change status because of it, and emits error with code engine. Upgrade the SDK to receive that configuration.
Deleted environments
When an environment or its project is deleted, the edge closes live connections with WebSocket close code 4004 and stops accepting the environment's keys within about a minute. The SDK then stops reconnecting, keeps evaluating its last configuration with status stale, and emits error with code environment_deleted. Remove or replace the key in your application.
Persistence and bootstrap
- Browser: IndexedDB when available (private modes and blocked storage behave like an empty store). Node and edge runtimes: no persistence by default, which is safe for read-only and ephemeral file systems.
storeaccepts any async string store;falsedisables it.- The stored value contains the bundle (flag rules), never the SDK key. It is bound to the key and
baseUrl, so a copy written for another environment is ignored. A restored bundle starts asstaleuntil revalidated. - With
verify, a stored bundle is kept with its signature and verified again when restored; withrequire, an unsigned stored copy is ignored. WithpublicKeys: "fetch"an offline start cannot verify (keys not loaded), so the stored copy is not restored; pinned keys do not have this limitation. bootstrapis trusted as given. For server rendering, pass the bundle you evaluated with on the server so the first client render matches.
import { readFile, writeFile } from "node:fs/promises";
const flags = createClient({
sdkKey, baseUrl,
store: {
get: () => readFile("/var/cache/nanoflags.json", "utf8").catch(() => null),
set: (v) => writeFile("/var/cache/nanoflags.json", v),
},
});Telemetry
Every flushIntervalMs (10 s) while evaluations are queued, the SDK sends one POST /v1/events; while nothing is queued it sends the service report alone every reportIntervalMs (60 s). A request carries a service report ({ service, sdk: "js/<version>", revision }) and up to 50 sampled evaluation events (sampleRate, default 10 %): flag key, value, reason, rule index, revision, time, and a redacted context.
{ "region": "string", "signedIn": "boolean", "key": "string" }. Attributes no rule uses are dropped.events.redact(ctx)replaces the default; whatever it returns is sent as is.- Flag values are sent (strings above 1 KB are truncated, larger JSON values are replaced by
null). Do not put secrets in flag values. - Telemetry is off the evaluation path: events go to a bounded in-memory queue (500 entries, then dropped). Failed batches are dropped, never retried.
events: falsedisables everything, including service reports.
What a reported signal proves, and what it does not
- A service report proves that a process using this SDK key, naming itself
service, had revision N loaded when it reported. It does not prove that every instance is on that revision, that the service is healthy, or that it is still running: it is "last seen". A missing report proves nothing: telemetry may be disabled, blocked, sampled out or dropped. serviceis self-declared and authenticated only by the SDK key. With a client key, anyone can send reports and events. Treat them as observability, not as an audit trail.- A sampled event proves that an evaluation with that result happened in some process. It does not prove that the user saw the feature, and the absence of events does not prove a flag is unused: with 10 % sampling, rarely evaluated flags can go unseen for a long time.
- Counts are estimates: sampling is per evaluation, events are dropped under back-pressure, network failure or the daily cap, and React components may evaluate once per relevant change rather than once per user action.