Betterflag docs
@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/sdk

Create a client

typescript
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.

typescript
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:

app/api/checkout/route.tstypescript
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:

typescript
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

MethodReturnsDescription
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)() => voidFires after a config change; returns an unsubscribe function.
ready()Promise<void>Resolves after the first snapshot fetch settles. Never rejects.
close()voidStops polling and drops all listeners.

Config options

OptionDefaultDescription
keyrequiredYour publishable SDK key (bf_sdk_*).
baseUrlhttps://api.betterflag.appThe edge evaluate endpoint.
refreshInterval30000Snapshot 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.
fetchglobal fetchCustom fetch implementation.
onErrorundefined(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.