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.
Install
Section titled “Install”npm install @feathq/web-sdk# oryarn add @feathq/web-sdkFor 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); // syncconst 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.
Reacting to flag changes
Section titled “Reacting to flag changes”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).
Streaming
Section titled “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 streamundefined(default) - streaming follows subscription: the stream opens when the firstchangelistener 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.
OpenFeature
Section titled “OpenFeature”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.
Server-side rendering
Section titled “Server-side rendering”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.
How it works
Section titled “How it works”- 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
Mapkeyed by flag, so reads are O(1) and synchronous afterready(). 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.
urlmust behttps://. Plaintext is rejected excepthttp://localhostfor tests.
Security notes
Section titled “Security notes”- 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) underfeat: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 tofeat:anonymousKey. Usestorage: "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.
Related
Section titled “Related”- API keys for what makes the client-side ID safe.
- Contexts for the input shape.
- OpenFeature for the standardized API.