Skip to content

Browser SDK

@feathq/web-sdk is the browser SDK. It evaluates flags remotely: it sends the current context to feat’s edge, receives the resolved values back, and caches them so your reads stay synchronous. Your targeting rules and segment definitions never reach the browser. A live stream keeps the cached values fresh. See Evaluation model for how remote evaluation works and why.

Terminal window
npm install @feathq/web-sdk
# or
yarn add @feathq/web-sdk

For server code use @feathq/js-sdk. For OpenFeature on the web, install @feathq/openfeature-web alongside this package.

import { FeatWebClient } from "@feathq/web-sdk";
const client = new FeatWebClient({
apiKey: "feat_cs_…", // client-side ID
url: "https://data-01.feat.so", // optional; this is the default
context: { targetingKey: "user-123" }, // optional: set now or via setContext()
anonymous: { storage: "localStorage" }, // optional: stable anonymous user
cache: { storage: "localStorage" }, // optional: warm reads across loads
});
await client.ready();
const enabled = client.getBooleanValue("checkout-v2", false); // sync
const greeting = client.getStringValue("hero-greeting", "Hi");

Use a client-side ID (feat_cs_…). It is safe to embed in your bundle. Add your site’s origin to the key’s authorized URLs in the feat console.

Values are evaluated on feat’s edge against the context you set, so reads return the default until a context is present. Pass context at construction, or call setContext before your first read.

client.on("change", ({ flagKey, newValue }) => {
console.log(`${flagKey} → ${newValue}`);
});
await client.setContext({
targetingKey: "user-123",
user: { plan: "pro" },
});

change fires per flag whose evaluated value flipped, after either a context change or a new snapshot from the server. Use it to re-render parts of your UI. Adding the first change listener opens the live stream (see Streaming).

The SDK keeps cached values current two ways: a background poll and a live Server-Sent Events stream. The server pushes a full evaluated snapshot on connect and deltas thereafter, adopted in version order.

new FeatWebClient({ apiKey, streaming: true }); // always stream
  • undefined (default) - streaming follows subscription: the stream opens when the first change listener is added and closes when the last is removed, so a page that never listens pays nothing.
  • true - always stream, opening once the client is ready.
  • false - never stream; rely on polling only.

Polling stays on as a safety net in every mode. It runs every 30 seconds by default (pollIntervalMs, floored at 5s) and relaxes to a slow cadence (~10 min) while the stream is connected, snapping back the moment it drops.

import { OpenFeature } from "@openfeature/web-sdk";
import { FeatWebClient } from "@feathq/web-sdk";
import { FeatWebProvider } from "@feathq/openfeature-web";
const featClient = new FeatWebClient({ apiKey });
await OpenFeature.setProviderAndWait(new FeatWebProvider(featClient));
await OpenFeature.setContext({ targetingKey: "user-123" });
const enabled = OpenFeature.getClient().getBooleanValue("checkout-v2", false);

See OpenFeature. The web provider forwards change events to OpenFeature so React hooks re-render on updates.

Evaluate the flags on the server for the incoming context and hand the resulting snapshot to the client, so the first paint has real values and skips the first round trip:

// Server: produce an evaluated snapshot for this request's context.
const snapshot = await evaluateSnapshotForContext(context); // { flags, version }
// Client: seed the SDK with it.
new FeatWebClient({ apiKey, context, bootstrap: snapshot });

bootstrap takes an evaluated snapshot ({ flags, version }), not the datafile. It must be produced for the same context the client starts with. getBooleanValue and friends then work synchronously from the first render. This is how the Next.js / React setup avoids a flash of default values on hydration.

  • Remote evaluation. The SDK POSTs the context to feat’s edge, which runs the evaluation engine and returns only the resolved value, variation, and reason per flag. The browser never receives rules or segments.
  • Synchronous reads. Resolved values are cached in a Map keyed by flag, so reads are O(1) and synchronous after ready(). New snapshots (from the stream or a poll) replace the map atomically.
  • Visibility-aware polling. Pauses while the tab is hidden. Force-refreshes when the tab regains visibility.
  • 304 / version-aware. Unchanged snapshots are skipped by version, so an idle tab does no redundant work.
  • url must be https://. Plaintext is rejected except http://localhost for tests.
  • Rules and segments never reach the browser. Evaluation happens on feat’s edge; the client only ever sees resolved values for the context it evaluated. There is no ruleset to inspect in the bundle or in storage.
  • cache: { storage: "localStorage" } persists the last evaluated snapshot (resolved values only, keyed by context) under feat:evalcache, so the next load can render immediately while the SDK refreshes. Default is off. The cached snapshot is ignored on load if the current context differs.
  • anonymous: { storage: "localStorage" } writes a stable UUID to feat:anonymousKey. Use storage: "memory" if you do not want it persisted.
  • The context you set is sent to feat’s edge to evaluate and counts toward your MAU, but is not stored or logged as an event.