SDKs
Flag’s SDK keeps the latest snapshot of an environment’s flags in memory and evaluates locally: no network call per evaluation. Snapshots arrive over a change stream (Server-Sent Events), with polling as a fallback, and the SDK keeps serving the last good snapshot if Flag is unreachable. It sends evaluation counts back in batches. Evaluations never throw: on any problem they return the default you passed, with a reason saying why.
| SDK | Install | Runs on | OpenFeature |
|---|---|---|---|
| TypeScript | npm install @nightroll/flag |
Node 20+, Bun, Deno, browsers, React 18+ | server and web providers |
| OFREP | your OpenFeature SDK’s OFREP provider | any language | yes |
The SDK and Flag’s OFREP endpoints implement the same evaluation spec and pass the same conformance vectors, so a context gets the same variation either way.
Keys
| Key | Looks like | Where | Sees |
|---|---|---|---|
| Server SDK key | srv_ + 40 characters |
back ends; keep it secret | the whole environment, as an encrypted snapshot |
| Client-side ID | cli_ + 24 characters |
browsers and apps; public | only flags marked available to client-side SDKs, with target keys hashed |
Make them under Environments. A server key is shown once. Each key belongs to one environment.
Contexts
A context is JSON: {"kind": "user", "key": "u-123", "email": "ada@acme.com", "plan": "pro"}. kind defaults to
user; key is required. For several kinds at once: {"kind": "multi", "user": {"key": "u-123"}, "org": {"key": "acme", "plan": "enterprise"}}. Rules reach nested fields with paths such as /address/city.
Private attributes: name attributes (or /a/b paths) in the SDK’s privateAttributes option, or per context in
_meta.privateAttributes, and they are removed before a context is sent in an event. Keys are always sent.
TypeScript
npm install @nightroll/flag
Server (Node, Bun, Deno):
import { FlagClient } from '@nightroll/flag';
const flags = new FlagClient({ sdkKey: process.env.FLAG_SDK_KEY });
await flags.ready(); // true once a snapshot is loaded; false after initTimeoutMs (it keeps trying)
const user = { kind: 'user', key: 'user-123', email: 'ada@example.com', plan: 'pro' };
const on = flags.boolVariation('new-checkout', false, user);
const model = flags.stringVariation('ai-model', 'small', user);
const detail = flags.numberVariationDetail('max-items', 10, user); // { value, variation, reason }
await flags.close(); // on shutdown: flushes events
Browsers take the client-side ID; set the context once with identify():
import { FlagClient } from '@nightroll/flag/web';
const flags = new FlagClient({ clientId: 'cli_...' });
flags.identify({ key: currentUser.id, plan: currentUser.plan });
await flags.ready();
const showBanner = flags.boolVariation('summer-banner', false);
flags.subscribe(() => rerender()); // after each new snapshot or identify()
React:
import { FlagClient } from '@nightroll/flag/web';
import { FlagProvider, useFlag, useFlagDetail } from '@nightroll/flag/react';
const flags = new FlagClient({ clientId: 'cli_...' }); // once, outside components
export function App({ user }) {
return (
<FlagProvider client={flags} context={{ key: user.id, plan: user.plan }}>
<Checkout />
</FlagProvider>
);
}
function Checkout() {
const newCheckout = useFlag('new-checkout', false); // re-renders when flags change
const { value: model, reason } = useFlagDetail('ai-model', 'small');
// ...
}
OpenFeature, server (@openfeature/server-sdk) and web (@openfeature/web-sdk):
import { OpenFeature } from '@openfeature/server-sdk';
import { FlagServerProvider } from '@nightroll/flag';
await OpenFeature.setProviderAndWait(new FlagServerProvider({ sdkKey: process.env.FLAG_SDK_KEY }));
const enabled = await OpenFeature.getClient().getBooleanValue('new-checkout', false, { targetingKey: 'user-123', plan: 'pro' });
import { OpenFeature } from '@openfeature/web-sdk';
import { FlagWebProvider } from '@nightroll/flag/web';
await OpenFeature.setContext({ targetingKey: currentUser.id, plan: currentUser.plan });
await OpenFeature.setProviderAndWait(new FlagWebProvider({ clientId: 'cli_...' }));
OpenFeature.getClient().getBooleanValue('summer-banner', false);
The providers also work with @openfeature/react-sdk.
| Option | Default | |
|---|---|---|
sdkKey / clientId |
one of the two | |
baseUrl |
https://flag.nightroll.app |
|
initTimeoutMs |
5000 |
how long ready() waits for the first snapshot |
sendEvents |
true |
evaluation summaries |
flushIntervalMs |
300000 |
also flushed at 500 queued events, on close() and when a browser page is hidden; each upload is billed as at least 100 events |
privateAttributes |
[] |
names or /a/b paths never sent |
cachePath |
Node 20.16+: keep the last snapshot (still encrypted) on disk and start from it when Flag is unreachable | |
fetch, logger |
global fetch, console |
OFREP (any OpenFeature SDK)
Flag speaks the OpenFeature Remote Evaluation Protocol, so any
OpenFeature SDK with an OFREP provider (Go, Python, Java, .NET, Rust and more) can use it: configure the provider with
the base URL https://flag.nightroll.app and the header Authorization: Bearer <server key or client-side ID>. Each
evaluation is a request (remote evaluation, metered per request), and it sees changes within about 60 seconds.
# one flag
curl https://flag.nightroll.app/ofrep/v1/evaluate/flags/new-checkout \
-H "Authorization: Bearer $FLAG_SDK_KEY" -H 'content-type: application/json' \
-d '{"context": {"targetingKey": "user-123", "plan": "pro"}}'
# {"key": "new-checkout", "value": true, "reason": "TARGETING_MATCH", "variant": "0", "metadata": {"flagVersion": 4}}
# every flag (client-side IDs see client-side flags only)
curl -X POST https://flag.nightroll.app/ofrep/v1/evaluate/flags \
-H "Authorization: Bearer $FLAG_CLIENT_ID" -H 'content-type: application/json' \
-d '{"context": {"targetingKey": "user-123"}}'
targetingKey becomes the context’s key; an attribute kind sets its kind. Reasons are DISABLED,
TARGETING_MATCH, SPLIT (a percentage rollout), DEFAULT, PREREQUISITE_FAILED and ERROR; the variant is the
variation’s index.
Events and insights
Every flush, the SDK sends counts of evaluations per flag, variation and version. They feed each flag’s Insights.
Flag keeps only these per-flag counts: the contexts your code evaluates are never stored. Other event kinds (track(),
identify()) are accepted and ignored in this beta.
How the SDK behaves
- At start it fetches the snapshot (waiting up to 5 s by default), then follows the change stream and fetches each new version; while the stream is down it polls every 30 seconds.
429answers are retried afterRetry-After; other failures back off from 1 s to 60 s with jitter. A401(a revoked key) stops updates and keeps serving the last snapshot.- Events go in batches of at most 500, each with a
batchIdthat its single retry reuses; at most 10,000 wait in memory.
The wire protocol, for writing your own SDK: SDK protocol.
This page as Markdown: /docs/sdks.md · All docs for agents: /llms.txt