@betterflag/sdk Node + browser
JavaScript / TypeScript
@betterflag/sdk is a zero-dependency client for Node and the browser. Every flag check takes a default and hands it back on any failure, offline, on a typo, or during an outage - so it never throws and never blocks your request path.
Install
npm i @betterflag/sdkCreate a client
import { createClient } from "@betterflag/sdk";
const betterflag = createClient({ key: "bf_sdk_..." });Checking a flag
flag() returns the value or your default. Flags can be any JSON type, boolean, string, number, or object.
const useV2 = await betterflag.flag("checkout-v2", {
userId: "u_42",
attributes: { plan: "pro", region: "eu" },
default: false,
});
const theme = await betterflag.flag("theme", { userId, default: "light" });
const limit = await betterflag.flag("rate-limit", { default: 100 });
const retry = await betterflag.flag("retry-config", {
default: { retries: 3 },
});No try/catch
Because flag() never throws, you never wrap it in a try/catch. If anything goes wrong you get your default - that's the whole contract.
Node example
A route handler gating a response behind a flag:
import { createClient } from "@betterflag/sdk";
const betterflag = createClient({ key: process.env.BETTERFLAG_SDK_KEY! });
export async function POST(req: Request) {
const { userId } = await req.json();
const useV2 = await betterflag.flag("checkout-v2", {
userId,
default: false,
});
return Response.json({ checkout: useV2 ? "v2" : "v1" });
}It won't hold your process open
In Node the refresh timer is unref'd, so the background poll never keeps the process alive. Call close() if you want to stop it explicitly.
Browser example
Provide defaults for an instant, offline-safe first render, then live-update when config changes:
import { createClient } from "@betterflag/sdk";
const betterflag = createClient({
key: "bf_sdk_...",
defaults: { "new-nav": false, theme: "light" },
});
// Re-render whenever the config version changes.
const unsubscribe = betterflag.on("update", () => {
render();
});
async function render() {
const newNav = await betterflag.flag("new-nav", { default: false });
document.body.classList.toggle("new-nav", newNav);
}
await betterflag.ready();
render();API methods
| Method | Returns | Description |
|---|---|---|
flag(key, opts) | Promise<T> | The flag value, or opts.default on ANY failure. Never throws, no try/catch needed. |
flagDetail(key, opts?) | Promise<EvaluationResult> | { value, reason, variation, ruleId?, bucket? }, the value plus why you got it. |
allFlags(context?) | Promise<Record<string, JsonValue>> | Every flag in the environment (client defaults, or {} on failure). |
on("update", cb) | () => void | Fires after a config change; returns an unsubscribe function. |
ready() | Promise<void> | Resolves after the first snapshot fetch settles. Never rejects. |
close() | void | Stops polling and drops all listeners. |
Config options
| Option | Default | Description |
|---|---|---|
key | required | Your publishable SDK key (bf_sdk_*). |
baseUrl | https://api.betterflag.app | The edge evaluate endpoint. |
refreshInterval | 30000 | Snapshot poll cadence + eval-cache TTL, in ms. Clamped ≥5000; 0 disables polling. |
defaults | {} | Record of offline fallbacks used before the first fetch and on failure. |
fetch | global fetch | Custom fetch implementation. |
onError | undefined | (err) => void, observe swallowed errors without changing control flow. |
How it works
- Evaluations POST to api.betterflag.app/v1/evaluate and are cached in-memory per (flag, userId, attributes) for refreshInterval.
- A background poll hits GET /v1/snapshot with If-None-Match. A 304 is nearly free.
- On a version change the cache clears and the "update" event fires, so your next check sees fresh values.
