@ferrow/feature-flags
v2.0.0
Published
Deterministic feature flag evaluation engine: boolean flags, percentage rollout via consistent hashing, attribute targeting rules (eq/in/gt/lt), environment overrides, and a pluggable async store.
Maintainers
Readme
feature-flags
A deterministic feature flag evaluation engine. Boolean flags, percentage rollouts via consistent hashing, attribute-based targeting rules, per-environment overrides, and a pluggable async store — with zero runtime dependencies.
Deterministic means what it sounds like: the same flag key + user id + attributes always evaluates to the same result, every time, on every process — no coin flips, no per-request randomness.
Install
npm install feature-flagsQuickstart
import { FeatureFlagEngine, InMemoryStore } from "feature-flags";
const engine = new FeatureFlagEngine(new InMemoryStore());
await engine.setFlag({
key: "new-checkout",
defaultValue: false,
rolloutPercentage: 30, // ~30% of users, stably bucketed
});
const enabled = await engine.isEnabled("new-checkout", { userId: "user-42" });API
new FeatureFlagEngine(store: FlagStore)
setFlag(definition: FlagDefinition): Promise<void>removeFlag(flagKey: string): Promise<void>listFlags(): Promise<FlagDefinition[]>evaluate(flagKey: string, ctx: EvaluationContext): Promise<EvaluationResult>isEnabled(flagKey: string, ctx: EvaluationContext): Promise<boolean>— convenience wrapper aroundevaluate.
FlagDefinition
interface FlagDefinition {
key: string;
defaultValue: boolean;
rules?: TargetingRule[]; // evaluated in order, first match wins
rolloutPercentage?: number; // 0-100
environmentOverrides?: Record<string, boolean>;
}
interface TargetingRule {
attribute: string; // read from ctx.attributes
operator: "eq" | "in" | "gt" | "lt";
value: unknown; // array for "in", scalar otherwise
serve: boolean;
}Evaluation order
environmentOverrides[ctx.environment], if set and present.rules, in array order — first matching rule wins.rolloutPercentage, via consistent hashing (see below).defaultValue.
EvaluationResult.reason tells you which of these decided the outcome
("environment_override" | "rule_match" | "rollout" | "default" | "flag_not_found").
FlagStore (pluggable)
interface FlagStore {
get(flagKey: string): Promise<FlagDefinition | undefined>;
set(flagKey: string, definition: FlagDefinition): Promise<void>;
delete(flagKey: string): Promise<void>;
list(): Promise<FlagDefinition[]>;
}InMemoryStore is bundled. Implement the interface yourself for
Postgres/Redis/a config file/whatever you already have.
fnv1a / bucketOf
Exported for testing or building your own rollout logic:
bucketOf(flagKey, userId) returns a stable integer in [0, 10000)
derived from FNV-1a of "${flagKey}:${userId}".
Design notes
Rollout uses FNV-1a hashing rather than Math.random() specifically so
the same user always lands in the same bucket for a given flag — that's
what makes a "30% rollout" actually mean something across page loads,
servers, and days, instead of re-flipping a coin every evaluation. The
store is an interface, not a bundled database client, because flag state
belongs wherever the rest of your app's config already lives; shipping
our own Postgres/Redis dependency would mean forcing infrastructure
choices on you that have nothing to do with flag evaluation logic.
Sponsored by Ferrow
Part of the ferrow-toolkit collection · Sponsored by Ferrow
