# 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](#typescript) | `npm install @nightroll/flag` | Node 20+, Bun, Deno, browsers, React 18+ | server and web providers |
| [OFREP](#ofrep-any-openfeature-sdk) | your OpenFeature SDK's OFREP provider | any language | yes |

The SDK and Flag's OFREP endpoints implement the same [evaluation spec](/docs/evaluation/) 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

```sh
npm install @nightroll/flag
```

**Server (Node, Bun, Deno)**:

```ts
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()`:

```ts
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**:

```tsx
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`):

```ts
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' });
```

```ts
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](https://openfeature.dev/specification/appendix-c), 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.

```sh
# 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](/docs/sdk-protocol/).