Docs
/
Packages
/
@zudojs/feature-flags
v1.4.0
@zudojs/feature-flags
Feature flag system with deterministic rollouts, a rule engine, multiple providers, variants, snapshots, and evaluation context. Toggle features without redeploying.
FEATURE FLAGS
ROLLOUTS
TARGETING
VARIANTS
INSTALLATION
// npm
npm install @zudojs/feature-flags
// pnpm
pnpm add @zudojs/feature-flags
// yarn
yarn add @zudojs/feature-flags
Peer Dependencies: @zudojs/feature-flags depends on @zudojs/errors (v1.3.0) for its error hierarchy and @zudojs/types (v1.2.0) for its shared type guards.
WHAT IT DOES
@zudojs/feature-flags gives you a complete feature flag system. It defines:
- → Evaluation engine — deterministic flag evaluation with context-aware targeting
- → Rule engine — 7 rule types: static, user, tenant, attribute, percentage, schedule, variant
- → Multiple providers — in-memory, environment variables, composite, and cached providers
- → Rollout bucketing — deterministic FNV-1a hashing for consistent percentage rollouts
- → Variant assignment — weighted variant assignment for A/B testing
- → Dependency resolution — a dependent flag is on only where each prerequisite evaluates on for the same context (state, expiry, rules, rollout), with cycle detection
- → Snapshots — evaluate all flags at once for a given context
Core Principle: Feature flags are evaluated locally — no network calls at evaluation time. Providers fetch definitions; the evaluator runs synchronously with deterministic results.
WHERE IT SITS
TRANSPORT LAYER (HTTP, CLI, WebSocket)
FEATURE FLAGS (@zudojs/feature-flags)
APPLICATION LAYER (CQRS, Modules, Services)
INFRASTRUCTURE (Container, Config, Logger)
Feature flags sit between transport and application layers. HTTP handlers check flags before executing routes. Application services gate features. Infrastructure observes for logging.
DEPENDENCIES
| Package |
Version |
Purpose |
| @zudojs/errors |
1.3.0 |
Feature flag error hierarchy (FeatureFlagError, NotFoundError, ProviderError, etc.) |
| @zudojs/types |
1.2.0 |
Shared type guards (isPlainObject, re-exported here and deprecated in favour of importing it from @zudojs/types) |
CORE TYPES
FeatureFlagValue
A feature flag value — boolean for simple toggles, string/number for variants.
type FeatureFlagValue = boolean | string | number | null | Record<string, unknown>;
FeatureFlagState
Feature flag lifecycle state.
type FeatureFlagState = "active" | "disabled" | "archived" | "draft";
FeatureFlagVisibility
Feature flag visibility scope.
type FeatureFlagVisibility = "server" | "client";
FeatureFlagContext
Context passed to flag evaluators for targeting decisions.
interface FeatureFlagContext {
readonly userId?: string;
readonly tenantId?: string;
readonly sessionId?: string;
readonly environment?: string;
readonly attributes?: Readonly<Record<string, unknown>>;
}
FeatureFlag
A complete feature flag definition.
interface FeatureFlag {
readonly key: string;
readonly defaultValue: FeatureFlagValue;
readonly enabled: boolean; // the kill switch
readonly offValue?: FeatureFlagValue; // served while the flag is off (new in v1.4.0)
readonly description?: string;
readonly state?: FeatureFlagState;
readonly visibility?: FeatureFlagVisibility;
readonly rules?: Readonly<FeatureFlagRule[]>;
readonly variants?: Readonly<FeatureFlagVariant[]>;
readonly dependencies?: Readonly<string[]>;
readonly metadata?: FeatureFlagMetadata;
}
When a flag is off
A flag is off when it has enabled: false or state: "disabled", is a "draft", is "archived" or past metadata.expiresAt, or is blocked by a dependency. An off flag skips its rules and serves its off value:
- 1.
offValue, when the flag declares one;
- 2. otherwise
false for a boolean flag, so the kill switch always turns a boolean feature off;
- 3. otherwise
defaultValue for a string, number or object flag, which has no natural "off".
import { createFeatureFlags, createMemoryProvider } from "@zudojs/feature-flags";
const flags = createFeatureFlags({
provider: createMemoryProvider([
{ key: "new-checkout", enabled: false, defaultValue: true },
{ key: "theme", enabled: false, defaultValue: "blue", offValue: "grey" },
{ key: "legacy-api", enabled: false, defaultValue: false, offValue: true },
]),
});
const checkout = await flags.evaluate("new-checkout");
console.log(checkout.value, checkout.reason); // false disabled
console.log((await flags.evaluate("theme")).value); // grey
console.log(await flags.isEnabled("legacy-api")); // true
Changed in v1.4.0 (behaviour change): the kill switch now fails closed. Up to v1.3.x an off flag served defaultValue, so { enabled: false, defaultValue: true } stayed on for everyone, and state: "disabled" was not honoured at all. If you relied on a killed flag serving true, declare offValue: true. defaultValue is still what an on flag serves when no rule matches.
FeatureFlagMetadata
Metadata about a feature flag.
interface FeatureFlagMetadata {
readonly owner?: string;
readonly team?: string;
readonly createdAt?: Date;
readonly updatedAt?: Date;
readonly expiresAt?: Date;
readonly ticket?: string;
readonly tags?: Readonly<string[]>;
}
RULE TYPES
Rules determine how flags are evaluated. Each rule type targets differently.
| Rule Type |
Interface |
Description |
| static |
FeatureFlagStaticRule |
Always returns its value. No targeting. |
| user |
FeatureFlagUserRule |
Targets specific users by ID list. |
| tenant |
FeatureFlagTenantRule |
Targets specific tenants by ID list. |
| attribute |
FeatureFlagAttributeRule |
Targets by attribute matching with operators. |
| percentage |
FeatureFlagPercentageRule |
Percentage-based rollout. Deterministic per subject. |
| schedule |
FeatureFlagScheduleRule |
Time-windowed rule. Enabled only within date range. |
| variant |
FeatureFlagVariantRule |
Assigns a variant key based on weight. |
Rule Interfaces
// Static — always returns its value
interface FeatureFlagStaticRule {
readonly type: "static";
readonly value: FeatureFlagValue;
}
// User — targets specific users by ID
interface FeatureFlagUserRule {
readonly type: "user";
readonly users: Readonly<string[]>;
readonly value: FeatureFlagValue;
}
// Tenant — targets specific tenants by ID
interface FeatureFlagTenantRule {
readonly type: "tenant";
readonly tenants: Readonly<string[]>;
readonly value: FeatureFlagValue;
}
// Attribute — targets by attribute matching
interface FeatureFlagAttributeRule {
readonly type: "attribute";
readonly attribute: string;
readonly operator: FeatureFlagOperator;
readonly value: unknown;
readonly result?: FeatureFlagValue; // served on match, default true
}
// Percentage — deterministic rollout
interface FeatureFlagPercentageRule {
readonly type: "percentage";
readonly percentage: number;
readonly value: FeatureFlagValue;
}
// Schedule — time-windowed
interface FeatureFlagScheduleRule {
readonly type: "schedule";
readonly startAt: string;
readonly endAt: string;
readonly value: FeatureFlagValue;
}
// Variant — weighted variant assignment
interface FeatureFlagVariantRule {
readonly type: "variant";
readonly variants: Readonly<FeatureFlagVariant[]>;
}
interface FeatureFlagVariant {
readonly key: string;
readonly weight: number;
}
// Union of all rule types
type FeatureFlagRule =
| FeatureFlagStaticRule
| FeatureFlagUserRule
| FeatureFlagTenantRule
| FeatureFlagAttributeRule
| FeatureFlagPercentageRule
| FeatureFlagScheduleRule
| FeatureFlagVariantRule;
Attribute Operators
type FeatureFlagOperator =
| "equals" // exact match
| "not_equals" // not equal
| "contains" // string contains
| "starts_with" // string prefix
| "ends_with" // string suffix
| "in" // value in list
| "not_in" // value not in list
| "greater_than" // numeric >
| "greater_than_or_equal" // numeric >=
| "less_than" // numeric <
| "less_than_or_equal" // numeric <=
| "exists" // attribute exists
| "matches"; // regex match; unsafe (nested-repetition) patterns and inputs > 1024 chars never match
Rule Evaluation
interface RuleEvaluationResult {
readonly matched: boolean;
readonly value?: FeatureFlagValue;
readonly variant?: string;
}
function evaluateRule(
rule: FeatureFlagRule,
context: FeatureFlagContext,
flagKey: string,
): RuleEvaluationResult;
First Match Wins: Rules are evaluated in order. The first matching rule determines the flag value. If no rules match, the defaultValue is used.
EVALUATION
FeatureFlagEvaluation
The result of evaluating a feature flag.
interface FeatureFlagEvaluation<TValue extends FeatureFlagValue = FeatureFlagValue> {
readonly key: string;
readonly value: TValue;
readonly reason: FeatureFlagEvaluationReason;
readonly matchedRule?: number;
readonly variant?: string;
readonly defaulted: boolean;
}
Evaluation Reasons
| Reason |
Meaning |
| default | No rules matched; used defaultValue |
| static | A static rule matched |
| rule_match | A rule matched by condition |
| target_match | User or tenant rule matched |
| percentage_rollout | Percentage rollout matched |
| variant_assignment | Variant assigned |
| disabled | Flag is off: enabled: false, state: "disabled" or state: "draft". Serves the off value. |
| not_found | Flag does not exist |
| error | Provider unreachable (provider.getAll() or provider.get() threw); reported to onError. This is how evaluate() reports an outage — snapshot() and getAll() reject instead. |
| dependency_disabled | A required dependency is not on for this context (disabled, draft, archived, expired, or evaluates false). Serves the off value. |
| expired | Flag is state: "archived" or metadata.expiresAt is in the past (a schedule rule outside its window simply does not match). Serves the off value. |
evaluateFlag
Evaluate a single feature flag against a context.
function evaluateFlag<TValue extends FeatureFlagValue = FeatureFlagValue>(
flag: FeatureFlag,
context?: FeatureFlagContext,
): FeatureFlagEvaluation<TValue>;
Using evaluateRule Directly
Evaluate a single rule against a context.
import { evaluateRule } from "@zudojs/feature-flags";
const result = evaluateRule(
{ type: "percentage", percentage: 50, value: true },
{ userId: "user-123" },
"my-flag",
);
// result: { matched: boolean, value?: boolean }
MAIN API — createFeatureFlags
Options
interface FeatureFlagsOptions {
readonly provider: FeatureFlagProvider;
readonly defaultContext?: FeatureFlagContext;
readonly throwOnMissing?: boolean; // default: false
readonly onError?: (error: unknown, source: string) => void;
readonly throwOnProviderError?: boolean; // default: false
readonly missingFlagTtlMs?: number; // default: 30_000 — how long a missing flag is remembered
readonly providerCooloffMs?: number; // default: 5_000 — how long a failing provider is left alone
}
| Option |
Default |
What it does |
| throwOnMissing |
false |
Throw FeatureFlagNotFoundError instead of reporting reason: "not_found". |
| onError |
none |
Receives every contained provider failure, with the call that produced it (FeatureFlagProvider.getAll, .get, .refresh). |
| throwOnProviderError |
false |
Rethrow provider failures instead of containing them, so an unreachable store is a hard failure the caller handles. |
| missingFlagTtlMs |
30_000 |
How long a key the provider does not know is remembered as missing. At most 1,000 keys; dropped on every reload. 0 asks the provider on every evaluation. |
| providerCooloffMs |
5_000 |
How long a failing provider is left alone before it is probed again. 0 restores the pre-v1.3.0 behaviour of calling the provider on every evaluation. A successful call closes the window at once, and refresh() always probes regardless. |
New in v1.3.0, providerCooloffMs exists because an outage used to cost two remote round trips per evaluation: every single evaluate() re-ran getAll() and get(key) against the store that had just failed, each waiting out its own timeout. With the default 5,000 ms window the provider is probed at most once per window while it is down.
Returned API
function createFeatureFlags(options: FeatureFlagsOptions): {
// Check if a flag is enabled (boolean shorthand)
isEnabled(key: string, context?: FeatureFlagContext): Promise<boolean>;
// Get a flag value with optional type parameter
get<T extends FeatureFlagValue = FeatureFlagValue>(
key: string, context?: FeatureFlagContext
): Promise<T | undefined>;
// Get a boolean flag with a default fallback
getBoolean(
key: string, defaultValue: boolean, context?: FeatureFlagContext
): Promise<boolean>;
// Full evaluation with reason and metadata
evaluate<T extends FeatureFlagValue = FeatureFlagValue>(
key: string, context?: FeatureFlagContext
): Promise<FeatureFlagEvaluation<T>>;
// Evaluate every client-visible flag at once.
// Rejects with FeatureFlagProviderError if the flags never loaded.
snapshot(context?: FeatureFlagContext): Promise<ReadonlyMap<string, FeatureFlagEvaluation>>;
// Refresh flags from provider
refresh(): Promise<void>;
// Get all flag definitions.
// Rejects with FeatureFlagProviderError if the flags never loaded.
getAll(): Promise<Readonly<FeatureFlag[]>>;
// Stop listening for provider changes
close(): void;
};
When the provider is unreachable
The two halves of the API deliberately behave differently, and v1.3.0 widened the gap. Get this the wrong way round and a total outage ships to a browser as every flag being off.
| Call |
Provider down, nothing ever loaded |
| evaluate() |
Resolves with reason: "error", defaulted: true. It does not throw. Unchanged in v1.3.0. |
| isEnabled() / get() / getBoolean() |
Resolve. They delegate to evaluate(), so they fall back to false, undefined and the defaultValue you passed. |
| snapshot() |
Rejects with FeatureFlagProviderError. Before v1.3.0 it resolved to an empty Map. |
| getAll() |
Rejects with FeatureFlagProviderError. Before v1.3.0 it resolved to an empty array. |
Why the asymmetry: an evaluation has somewhere to put the bad news — the reason field on the result it returns. A Map or an array has nowhere, and an empty one is indistinguishable from “no flags are configured”. So the bulk reads now fail loudly instead of quietly reporting an outage as a configuration state. onError still sees the underlying failure first, either way.
Two qualifications. Once a load has succeeded, snapshot() and getAll() keep serving that data even if a later reload fails — only a cold, never-loaded instance rejects. And a provider that genuinely holds no flags still resolves empty, because the load succeeded.
// Upgrading from v1.2.x: this used to be dead code on a cold outage.
try {
const visible = await flags.snapshot(context);
res.json(Object.fromEntries(visible));
} catch (error) {
if (error instanceof FeatureFlagProviderError) {
// Serve the last good payload, or fail the request —
// but do not ship "{}" and call it the flag state.
res.status(503).end();
return;
}
throw error;
}
Full Example
import { createFeatureFlags, createMemoryProvider } from "@zudojs/feature-flags";
const provider = createMemoryProvider([
{
key: "dark-mode",
defaultValue: false,
enabled: true,
description: "Enable dark mode UI",
state: "active",
visibility: "client",
},
{
key: "new-checkout",
defaultValue: false,
enabled: true,
state: "active",
visibility: "client",
rules: [
{
type: "percentage",
percentage: 25,
value: true,
},
],
},
]);
const flags = createFeatureFlags({ provider });
// Simple boolean check
const darkMode = await flags.isEnabled("dark-mode");
// Get value with type
const checkout = await flags.get<boolean>("new-checkout");
// Full evaluation with reason
const result = await flags.evaluate("new-checkout", {
userId: "user-42",
});
// result: { key, value, reason: "percentage_rollout", defaulted: false }
// Snapshot every client-visible flag. Both flags above declare
// visibility: "client"; a flag that does not is withheld, because
// a snapshot is what you ship to a browser.
// Rejects with FeatureFlagProviderError if the store never loaded.
const all = await flags.snapshot({ userId: "user-42" });
PROVIDERS
Providers fetch feature flag definitions from a source. Chain them with composite and cached for production use.
FeatureFlagProvider Interface
interface FeatureFlagProvider {
get(key: string): Promise<FeatureFlag | undefined>;
getAll(): Promise<Readonly<FeatureFlag[]>>;
refresh?(): Promise<void>;
subscribe?(listener: FeatureFlagChangeListener): Unsubscribe;
}
type FeatureFlagChangeListener = (flags: Readonly<FeatureFlag[]>) => void;
type Unsubscribe = () => void;
createMemoryProvider
In-memory provider. Flags stored in a Map. Also exposes set() and delete().
function createMemoryProvider(
flags?: Readonly<FeatureFlag[]>
): FeatureFlagProvider & {
set(flag: FeatureFlag): void;
delete(key: string): boolean;
};
import { createMemoryProvider } from "@zudojs/feature-flags";
const provider = createMemoryProvider();
provider.set({
key: "new-ui",
defaultValue: false,
enabled: true,
});
provider.delete("new-ui");
createEnvironmentProvider
Reads FEATURE_<KEY> environment variables. Parses booleans, numbers, and strings automatically, and sets both defaultValue and enabled: true from the variable, so FEATURE_X=false is how you switch it off. The key is the variable name with the prefix removed, lower-cased, and _ turned into -: FEATURE_TASK_EXPORT becomes the flag "task-export", the same key a memory or remote provider uses, so the variable overrides that flag in a composite. get() normalizes the key it is asked for the same way, so "TASK_EXPORT", "task_export" and "task-export" all find the flag.
interface EnvironmentProviderOptions {
readonly prefix?: string; // default: "FEATURE_"
readonly env?: Readonly<Record<string, string | undefined>>;
readonly keyFormat?: "kebab" | "preserve"; // default: "kebab"
}
function createEnvironmentProvider(
options?: EnvironmentProviderOptions
): FeatureFlagProvider;
// FEATURE_DARK_MODE=true FEATURE_ROLLOUT_PERCENT=25
const provider = createEnvironmentProvider();
const flag = await provider.get("dark-mode");
// flag: { key: "dark-mode", enabled: true, defaultValue: true }
// get("DARK_MODE") finds the same flag; getAll() keys are "dark-mode", "rollout-percent"
Changed in v1.4.0: up to v1.3.x the key kept its case (FEATURE_TASK_EXPORT was the flag "TASK_EXPORT"), so an environment variable never overrode the "task-export" flag from another provider. Only the keys returned by getAll() and snapshot() change; lookups by the old spelling still work. Pass keyFormat: "preserve" to keep the old keys, which also turns off the lookup normalization.
createCompositeProvider
Queries providers in order. First provider to return a flag wins. getAll() merges with earlier providers taking priority.
function createCompositeProvider(
providers: Readonly<FeatureFlagProvider[]>
): RefreshableFeatureFlagProvider; // forwards subscribe() from members that offer it
const provider = createCompositeProvider([
createEnvironmentProvider(), // highest priority
createMemoryProvider(flags), // fallback
]);
createCachedProvider
Wraps a provider with in-memory TTL caching.
interface CachedProviderOptions {
readonly ttl?: number; // default: 30_000 (ms)
readonly maxEntries?: number; // default: 1000 cached flags
}
function createCachedProvider(
inner: FeatureFlagProvider,
options?: CachedProviderOptions
): RefreshableFeatureFlagProvider; // forwards subscribe() from the inner provider
const provider = createCachedProvider(
createEnvironmentProvider(),
{ ttl: 60_000 } // cache for 60 seconds
);
Production Pattern
import {
createFeatureFlags,
createCompositeProvider,
createCachedProvider,
createEnvironmentProvider,
createMemoryProvider,
} from "@zudojs/feature-flags";
const provider = createCachedProvider(
createCompositeProvider([
createEnvironmentProvider(),
createMemoryProvider(flags),
]),
{ ttl: 30_000 }
);
const featureFlags = createFeatureFlags({
provider,
defaultContext: { environment: "production" },
throwOnMissing: false,
// Leave a failing store alone for 5 s instead of re-querying it
// on every evaluation. 5_000 is the default; 0 disables the window.
providerCooloffMs: 5_000,
onError: (error, source) => console.error(source, error),
});
REGISTRY
An in-memory feature flag registry with O(1) lookup by key.
FeatureFlagRegistry Interface
interface FeatureFlagRegistry {
get(key: string): FeatureFlag | undefined;
getAll(): Readonly<FeatureFlag[]>;
set(flag: FeatureFlag): void;
setAll(flags: Readonly<FeatureFlag[]>): void;
delete(key: string): boolean;
has(key: string): boolean;
readonly size: number;
}
function createFeatureFlagRegistry(
flags?: Readonly<FeatureFlag[]>
): FeatureFlagRegistry;
import { createFeatureFlagRegistry } from "@zudojs/feature-flags";
const registry = createFeatureFlagRegistry([
{ key: "flag-a", defaultValue: true, enabled: true },
{ key: "flag-b", defaultValue: false, enabled: true },
]);
registry.has("flag-a"); // true
registry.get("flag-a"); // FeatureFlag object
registry.size; // 2
ROLLOUT UTILITIES
Deterministic FNV-1a hashing for consistent percentage rollouts.
hashString
Compute a deterministic 32-bit unsigned hash using FNV-1a.
function hashString(value: string): number;
getBucket
Compute a deterministic bucket for a flag rollout.
function getBucket(
key: string,
subject: string,
buckets?: number, // default: 10_000
): number; // returns value in [0, buckets)
isInRollout
Check whether a subject falls within a percentage rollout.
function isInRollout(
key: string,
subject: string,
percentage: number, // 0 to 100, supports decimals like 25.5
): boolean;
import { isInRollout, getBucket } from "@zudojs/feature-flags";
// Same user always gets the same result
isInRollout("new-feature", "user-42", 25); // true or false, deterministic
isInRollout("new-feature", "user-42", 25); // same result every time
// Check exact bucket
getBucket("new-feature", "user-42"); // e.g. 4217
Deterministic: The same (flag key, subject) pair always produces the same bucket. This means users consistently see the same variant without server-side state.
ATTRIBUTE HELPERS
resolvePath
Safely resolve a dot-notation path from an object.
function resolvePath(obj: unknown, path: string): unknown;
resolvePath({ user: { country: "NG" } }, "user.country"); // "NG"
resolvePath({ user: {} }, "user.country"); // undefined
matchAttribute
Evaluate an attribute rule against a context value.
function matchAttribute(
actual: unknown,
operator: FeatureFlagOperator,
expected: unknown,
): boolean;
matchAttribute("NG", "equals", "NG"); // true
matchAttribute("Lagos", "contains", "Lag"); // true
matchAttribute(25, "greater_than", 18); // true
matchAttribute("free", "in", ["free", "trial"]); // true
ERROR HIERARCHY
All errors extend FeatureFlagError which extends ApplicationError from @zudojs/errors.
| Error Class |
When Thrown |
| FeatureFlagNotFoundError |
Requested flag does not exist and throwOnMissing: true |
| FeatureFlagProviderError |
Provider fails to fetch or parse flag definitions. Since v1.3.0 also raised by snapshot() and getAll() when the flags were never loaded, and by any call under throwOnProviderError: true. |
| FeatureFlagEvaluationError |
Evaluation encounters an error during rule processing |
| FeatureFlagRuleError |
A flag rule is malformed or has invalid configuration |
| FeatureFlagDependencyError |
Flag dependencies form a cycle |
| FeatureFlagConfigurationError |
Flag configuration is invalid |
| FeatureFlagTypeError |
Flag value has an unexpected type |
Error Handling Pattern
import {
FeatureFlagError,
FeatureFlagNotFoundError,
FeatureFlagProviderError,
} from "@zudojs/feature-flags";
try {
// A bulk read: this is the call that raises
// FeatureFlagProviderError on an unreachable store.
const all = await flags.getAll();
} catch (error) {
if (error instanceof FeatureFlagNotFoundError) {
// Flag doesn't exist (throwOnMissing: true)
} else if (error instanceof FeatureFlagProviderError) {
// Provider failed
} else if (error instanceof FeatureFlagError) {
// Any other feature flag error
}
}
Wrapping isEnabled(), get(), getBoolean() or evaluate() in a try for a provider failure catches nothing: they contain it and report reason: "error". Read evaluation.reason, pass onError, or set throwOnProviderError: true if you want them to throw. Only throwOnMissing: true makes them raise FeatureFlagNotFoundError.
DEPENDENCY RESOLUTION
Flags can depend on other flags. Dependencies are resolved recursively with cycle detection.
Defining Dependencies
const flags = [
{
key: "base-feature",
defaultValue: true,
enabled: true,
},
{
key: "advanced-feature",
defaultValue: false,
enabled: true,
dependencies: ["base-feature"], // requires base-feature to be enabled
rules: [
{ type: "percentage", percentage: 50, value: true },
],
},
];
How It Works
- → When evaluating
advanced-feature, the evaluator checks base-feature first
- → If
base-feature is not on for the same context (disabled, draft, archived, expired, or evaluates false), the result reason is dependency_disabled
- → Circular dependencies throw
FeatureFlagDependencyError
- → Dependencies are resolved via the same provider, not the registry
UTILITY FUNCTIONS
isPlainObject
function isPlainObject(value: unknown): value is Record<string, unknown>;
Re-export of isPlainObject from @zudojs/types (deprecated here; import it from types). Returns false for Date and Map.
valuesEqual
function valuesEqual(
a: FeatureFlagValue,
b: FeatureFlagValue
): boolean;
FULL INTEGRATION EXAMPLE
Complete working example combining providers, targeting, rollouts, variants, and error handling.
import {
createFeatureFlags,
createMemoryProvider,
createEnvironmentProvider,
createCompositeProvider,
createCachedProvider,
type FeatureFlag,
} from "@zudojs/feature-flags";
// 1. Define flags
const flags: FeatureFlag[] = [
{
key: "dark-mode",
defaultValue: false,
enabled: true,
description: "Enable dark mode UI",
state: "active",
visibility: "client",
},
{
key: "new-checkout",
defaultValue: false,
enabled: true,
state: "active",
rules: [
{ type: "user", users: ["user-1", "user-2"], value: true },
{ type: "percentage", percentage: 25, value: true },
],
},
{
key: "pricing-tier",
defaultValue: "free",
enabled: true,
state: "active",
rules: [
{
type: "attribute",
attribute: "plan",
operator: "equals",
value: "enterprise",
},
],
variants: [
{ key: "control", weight: 50 },
{ key: "treatment", weight: 50 },
],
},
];
// 2. Create provider chain
const provider = createCachedProvider(
createCompositeProvider([
createEnvironmentProvider(),
createMemoryProvider(flags),
]),
{ ttl: 30_000 }
);
// 3. Create feature flags instance
const featureFlags = createFeatureFlags({
provider,
defaultContext: { environment: "production" },
});
// 4. Use in your application
async function handleRequest(userId: string, plan: string) {
const context = { userId, attributes: { plan } };
// Simple boolean check
if (await featureFlags.isEnabled("dark-mode", context)) {
// apply dark mode
}
// Full evaluation
const checkout = await featureFlags.evaluate("new-checkout", context);
if (checkout.value) {
// show new checkout flow
}
// Variant assignment
const tier = await featureFlags.evaluate<string>("pricing-tier", context);
if (tier.variant === "treatment") {
// apply treatment pricing
}
// Snapshot all flags
const allFlags = await featureFlags.snapshot(context);
console.log(Object.fromEntries(allFlags));
}
SOURCE LOCATION
Source Code
packages/feature-flags/src/
Test Suite
packages/feature-flags/tests/
Dependencies
@zudojs/errors (1.3.0), @zudojs/types (1.2.0)
COMPLETE EXPORT INDEX
Every name @zudojs/feature-flags exports from its package root at v1.3.0 — 55 in total, generated from the package’s own entry point rather than written by hand. The sections above explain the ones you reach for most; this is the exhaustive list, so nothing shipped is undocumented. Names not covered above are typically internal helpers and supporting types.
Show all 55 exports
Classes (8)
FeatureFlagConfigurationError FeatureFlagDependencyError FeatureFlagError FeatureFlagEvaluationError FeatureFlagNotFoundError FeatureFlagProviderError FeatureFlagRuleError FeatureFlagTypeError
Functions (17)
createCachedProvider createCompositeProvider createEnvironmentProvider createFeatureFlagRegistry createFeatureFlags createMemoryProvider evaluateFlag evaluateRule getBucket hashString isInRollout isPlainObject matchAttribute mergeContext resolveDependencies resolvePath valuesEqual
Interfaces (22)
CachedProviderOptions EnvironmentProviderOptions EvaluateFlagOptions FeatureFlag FeatureFlagAttributeRule FeatureFlagContext FeatureFlagErrorOptions FeatureFlagEvaluation FeatureFlagMetadata FeatureFlagPercentageRule FeatureFlagProvider FeatureFlagRegistry FeatureFlags FeatureFlagScheduleRule FeatureFlagsOptions FeatureFlagStaticRule FeatureFlagTenantRule FeatureFlagUserRule FeatureFlagVariant FeatureFlagVariantRule MemoryFeatureFlagProvider RuleEvaluationResult
Type aliases (8)
FeatureFlagChangeListener FeatureFlagEvaluationReason FeatureFlagOperator FeatureFlagRule FeatureFlagState FeatureFlagValue FeatureFlagVisibility Unsubscribe