Feature Flags
@bext-stack/framework/flags is a Pennant-style feature-flag client for PRISM
apps. bext already has a Rust flag engine behind /__bext/sdk/flags/evaluate,
but it's server-file configured and there was no typed client — apps hand-rolled
fetch(). This module gives you two modes, and they compose:
- Local, code-defined flags — define a flag as a boolean, a predicate over
the request context, or a deterministic, sticky
percentage()rollout. Pure, no server, unit-testable — this is the Pennant headline bext was missing. - Remote fall-through — for keys you didn't define locally, evaluate against the Feature Flag capability's SDK endpoint (when a provider is configured).
When To Use It#
- Ship code dark and turn it on for a fraction of users.
- Gate a beta behind an allowlist or an attribute.
- Run an A/B/n experiment with sticky assignment.
Defining & evaluating#
import { defineFlags, percentage, forUsers, forAttribute } from "@bext-stack/framework/flags";
const flags = defineFlags({
flags: {
"new-checkout": percentage(30), // sticky 30% rollout
"beta-banner": forUsers("u_1", "u_2"),
"holiday-theme": forAttribute("country", "FR"),
"priority-queue": (ctx) => ctx.attributes?.plan === "enterprise",
},
remote: { appId: "my-site" }, // optional server fallback
default: false,
});
if (await flags.isEnabled("new-checkout", { userId: user.id })) { /* … */ }
const arm = flags.variant("home-hero", ["control", "b", "c"], { userId: user.id });
| Member | Returns |
|---|---|
isEnabled(key, ctx?) |
Promise<boolean> — local wins, else remote, else default |
isEnabledSync(key, ctx?) |
boolean — local only (remote keys → default) |
variant(experiment, variants, ctx?) |
a sticky arm for the subject |
all(ctx?) |
{ flag: boolean } for every local flag — bootstrap a UI |
defined |
the local flag names |
Rule helpers: percentage(pct) (deterministic, salted by the flag key so two
50% flags don't enable the identical half), forUsers(...ids),
forAttribute(key, value), or any (ctx) => boolean.
Deterministic & sticky#
Percentage rollouts and variant assignment hash the subject (userId →
sessionId → none) with the flag/experiment key, so:
- the same user always gets the same answer (no flip-flop between requests),
- a user with no id can't be bucketed stickily, so a
percentageflag stays off for them (fail-closed), and - two independent flags at the same percentage don't enable the same cohort.
Prefer userId in the context for logged-in users, and a stable sessionId
(e.g. from a signed session) for anonymous ones — that
keeps a rollout sticky across a visitor's requests.
Relationship to the Feature Flag capability#
The Rust Feature Flag capability is the server-side, file/provider-configured engine (also powers experiment includes). This module is the in-code TypeScript client: define flags next to the code they gate, evaluate them with no round-trip, and fall through to the engine only for keys you leave to ops.
Try It#
The live feature-flags demo lets
you type a user id and watch which flags evaluate on — deterministic and sticky,
so the same name always resolves the same way. Source in
sites/demo/src/app/examples/feature-flags/page.tsx.
See Also#
- Feature Flag — the Rust engine this fronts.
- Experiment variants — server-side A/B includes.
- Application Toolkit — the rest of the TypeScript app layer.