Skip to content

Browser SDK Reference

TL;DR: Install @featureflip/browser, call client.boolVariation('flag-key', false), and you’re evaluating flags in the browser — context comes from identify().

The @featureflip/browser package is the framework-agnostic Featureflip client for browser applications — vanilla JS, Vue, Svelte, Solid, Lit, Alpine, jQuery, or anything else that runs in a <script> tag. (For React specifically, use @featureflip/react, which adds a provider, hooks, and StrictMode-safe lifecycle handling on top of this package.) Unlike the server SDKs, the Browser SDK is a client-side SDK: it receives pre-evaluated feature flag values from the Evaluation API rather than running an evaluator locally. This keeps the bundle small and prevents shipping your targeting rules to every visitor.

Real-time updates arrive via the browser’s native EventSource, which means authentication is passed through the URL query string (browsers prohibit setting custom Authorization headers on EventSource). The SDK handles the auth flow automatically; you just provide your client SDK key. After the initial snapshot, the server streams delta updates — only the flags that changed — and the SDK merges them into the local cache rather than replacing it. Flags removed via targeting rules or deleted in the dashboard are dropped from the snapshot in place.

The Browser SDK is singleton-by-construction: there is no public constructor, only the static factory FeatureflipClient.get(config). Every call with the same client key returns a handle pointing at one shared underlying client. This makes framework bindings, React StrictMode double-mounts, and per-component construction all harmless — they all resolve to one SSE connection and one flag store per key. See the Lifetime section for the full contract. identify() updates the active context (for example, after a user logs in) by closing the previous SSE stream and reopening with the new context. This is the right SDK for SPAs where you need percentage rollouts and targeting to react to login state in real time.

View source on GitHub

  • Any modern browser with fetch and EventSource support
  • Node.js 20.19.0+ (for build tooling)
Terminal window
npm install @featureflip/browser
import { FeatureflipClient } from '@featureflip/browser';
const client = FeatureflipClient.get({
clientKey: 'your-client-sdk-key',
});
await client.initialize();
const showBanner = client.boolVariation('show-banner', false);

FeatureflipClient.get() is the only way to obtain a client — the public constructor was removed in v2.0. See Lifetime for details.

FeatureflipClient.get(config: FeatureflipClientConfig)
OptionTypeDefaultDescription
clientKeystring(required)Client SDK key from your project settings
baseUrlstringhttps://eval.featureflip.ioEvaluation API base URL
contextRecord<string, unknown>{}Initial evaluation context (user attributes)
streamingbooleantrueEnable SSE streaming for real-time updates
initTimeoutnumber10000Timeout in ms for the initial evaluate request
sendEvaluationEventsbooleantrueReport which flags the app reads, so flags it no longer reads can be archived. See Read reporting
bootstrapRecord<string, unknown> | 'localStorage'undefinedValues to serve before the first server response. See Bootstrapping
wrappersstring[][]For libraries that wrap the SDK: name/version tokens sent after the SDK’s own identity

Call initialize() before reading any flag values. This fetches all client-visible flags from the server and opens an SSE connection if streaming is enabled.

const client = FeatureflipClient.get({
clientKey: 'your-client-sdk-key',
});
await client.initialize();

Pass bootstrap and the client starts with values instead of waiting for its first request. Give it an object of { flagKey: value } that your server evaluated while rendering the page, or 'localStorage' to reuse the values this browser received on its last visit. The client is initialized as soon as it’s created, initialize() resolves at once, and the fetch and stream then run as usual, replacing the bootstrap values and firing change for any that differ.

const client = FeatureflipClient.get({
clientKey: 'your-client-sdk-key',
context: window.__FLAG_CONTEXT__,
bootstrap: window.__FLAGS__, // or 'localStorage'
});
client.flagSource; // 'bootstrap' until the server answers, then 'server'
await client.initialize({ waitForServer: true }); // only if you need the server's values first

For server-side rendering, FeatureflipClient.forServerRender(config) returns a client that serves the bootstrap values, makes no requests and isn’t cached, so concurrent requests never share one. The bootstrapping guide covers both modes, matching the server and browser contexts, and a full Node.js example.

The Browser SDK is singleton-by-construction: you cannot new a client directly, and every FeatureflipClient.get(config) call with the same clientKey returns a handle pointing at one shared background client. This is intentional — a browser client maintains a long-lived SSE streaming connection and an in-memory flag store. Opening more than one per clientKey per page wastes bandwidth and causes flag updates to fan out unpredictably.

The factory refcounts the shared core:

  • The first get() for a given client key constructs the shared core and returns a handle with refcount 1.
  • Subsequent get() calls with the same key return a new handle and increment the refcount. The config argument on repeat calls is ignored; if the resolved config differs meaningfully from the first call, a warning is logged.
  • handle.close() decrements the refcount. When the refcount reaches zero, the shared core runs its real shutdown: closes the SSE stream and removes itself from the factory cache.
  • Double-closing the same handle is a no-op — the handle tracks its own disposed state and will not double-decrement.
  • A different client key always constructs an independent shared core.

This makes the SDK safe under any lifetime pattern:

  • React StrictMode: mount → cleanup → remount cycles return new handles over the same core. The cleanup’s close() drives the refcount from 2 → 1 (not 0), so the SSE connection stays alive across the simulated remount. This is exactly what @featureflip/react’s FeatureflipProvider relies on.
  • Per-component construction: framework bindings can call get() inside component render without worrying about duplicating SSE connections. Each call allocates a fresh handle, not a new network connection.
  • Micro-frontend architectures: independent React/Vue/Svelte islands sharing a client key on the same page share one underlying connection.

Use FeatureflipClient.forTesting(flags) to obtain a standalone test client that is not registered in the factory cache — each call returns an independent instance with no background connections.

All variation methods accept a flag key and a default value. The default is returned if the flag is missing or has an unexpected type.

const enabled = client.boolVariation('dark-mode', false);
const color = client.stringVariation('button-color', 'blue');
const limit = client.numberVariation('rate-limit', 100);
const config = client.jsonVariation<{ sidebar: boolean }>('ui-config', { sidebar: true });

Call identify() to re-evaluate all flags with a new user context — typically after login or when user attributes change. Emits change events for any flags whose values changed.

await client.identify({ user_id: '123', plan: 'pro' });

Since 2.11.0, the SDK reports the flags your app reads. Featureflip compares them with the flags it sends, so a flag that no client reads anymore can be marked stale and archived, and one your code still reads stays protected. Only the typed *Variation methods and flagDetail() count as reads. A given flag, variation and user goes out about once an hour, plus once more whenever a hidden page becomes visible again, batched within 30 seconds or sent when the page is hidden or closed.

Two numbers move when your apps upgrade. Client-side evaluation counts drop, because they now count reads. And a flag that is sent but never read can start showing as stale.

To opt out, pass sendEvaluationEvents: false. Featureflip then treats every flag it sends this client as in use, the pre-2.11.0 behavior.

Subscribe to lifecycle events.

EventDescription
'ready'Fired once, when the client first has values to serve, with { source } ('bootstrap', 'localStorage', 'server' or 'none')
'change'Fired when flag values change (receives a FlagChanges object)
'error'Fired on streaming or network errors
client.on('change', (changes) => {
console.log('Flags changed:', changes);
});

Unsubscribe from events.

Decrements the refcount on the shared core. When the last handle for a given client key is closed, the shared core closes the SSE connection and removes itself from the factory cache. Double-closing the same handle is a no-op. A closed handle serves the caller’s default from every accessor — it does not keep returning the last-known values, which could never refresh once the connection is gone.

client.close();

Use FeatureflipClient.forTesting() to create a client with predetermined flag values — no network calls, no initialization needed.

const client = FeatureflipClient.forTesting({
'show-banner': true,
'button-color': 'blue',
});
client.boolVariation('show-banner', false); // true
client.stringVariation('button-color', 'red'); // 'blue'