FlagBeta

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.
  • 429 answers are retried after Retry-After; other failures back off from 1 s to 60 s with jitter. A 401 (a revoked key) stops updates and keeps serving the last snapshot.
  • Events go in batches of at most 500, each with a batchId that 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