@roostjs/feature-flags

Feature flag evaluation backed by WorkOS with KV edge caching. FeatureFlag static API, scoped evaluation, per-request caching, middleware, providers, and service provider.

Installation

bun add @roostjs/feature-flags

Configuration

FeatureFlagServiceProvider auto-selects a provider based on available environment bindings.

WorkOS + KV (recommended): Set WORKOS_API_KEY and declare a KV namespace:

{
  "kv_namespaces": [
    { "binding": "FLAGS_KV", "id": "<your-kv-namespace-id>" }
  ]
}

WorkOS only: Set WORKOS_API_KEY — no KV binding required.

KV only: Omit WORKOS_API_KEY and set flags.provider: 'kv' in app config.

The default KV binding name is FLAGS_KV. Override via flags.kv in app config.

FeatureFlag API

FeatureFlag is a static class. Feature is an alias — both names are exported and interchangeable.

Static Methods

static for(context: FlagContext): ScopedFeatureFlag

Create a scoped evaluator that passes user/org context to the provider for targeted evaluation.

const result = await FeatureFlag.for({ userId: 'u_1', organizationId: 'org_1' }).active('beta');

static async active(flag: string, request?: Request): Promise<boolean>

Alias for isEnabled(). Returns true if the flag evaluates to a truthy value.

static async value<T>(flag: string, defaultValue?: T): Promise<T | null>

Returns the raw flag value cast to T. Returns defaultValue when the flag is missing and a default is provided. Throws FlagNotFoundError when the flag is missing and no default is given.

static async isEnabled(flag: string, request?: Request): Promise<boolean>

Returns true if the flag evaluates to a truthy value. Accepts an optional request to read from the per-request cache set by FeatureFlagMiddleware.

Truthy rules:

Swallows provider errors and returns false rather than throwing.

static async getValue<T extends FlagValue>(flag: string, request?: Request): Promise<T | null>

Returns the raw flag value cast to T. Throws FlagNotFoundError if the flag does not exist. Use isEnabled() when you only need a boolean.

static async set<T extends FlagValue>(flag: string, value: T): Promise<void>

Write a flag value to the store. Requires a writable FlagStore to be configured (available when KV is present). Throws if only a read-only provider is active.

static configure(flagStore: FlagStore): void

Set a legacy FlagStore directly. Clears any active provider. Called by FeatureFlagServiceProvider for KV-only mode; also useful in tests or custom bootstrapping.

static configureProvider(flagProvider: FlagProvider): void

Set a FlagProvider as the evaluation backend. Clears the legacy store.

static configureProviderWithStore(flagProvider: FlagProvider, flagStore: FlagStore): void

Set both a FlagProvider for reads and a FlagStore for writes. Used by FeatureFlagServiceProvider when WorkOS handles evaluation and KV handles set().

static fake(flags: Record<string, FlagValue>): void

Enable fake mode with a static set of flag values. All subsequent calls to isEnabled(), active(), getValue(), and value() read from the in-memory fake.

static restore(): void

Disable fake mode and clear the configured store and provider.

static assertChecked(flag: string): void

Assert that isEnabled(), active(), getValue(), or value() was called for flag during the test. Throws if fake() was not called first, or if the flag was not accessed.

ScopedFeatureFlag API

Returned by FeatureFlag.for(context).

async active(flag: string): Promise<boolean>

Evaluate flag with the bound context. Returns true if truthy.

async value<T>(flag: string, defaultValue?: T): Promise<T | null>

Return the raw value for flag with the bound context. Returns defaultValue when missing and a default is provided. Throws FlagNotFoundError otherwise.

FeatureFlagMiddleware API

Prefetches a set of flag values at the start of each request and attaches them to the request object. Downstream calls to FeatureFlag.isEnabled(flag, request) read from the cache without another provider round-trip.

constructor(flags: string[])

Pass the list of flag names to prefetch on every request.

async handle(request: Request, next: (r: Request) => Promise<Response>): Promise<Response>

Fetches all configured flags in parallel, populates the per-request cache, then calls next.

FeatureFlagServiceProvider API

Bootstraps FeatureFlag with the appropriate provider based on available environment bindings.

register(): void

Provider selection logic:

WORKOS_API_KEYKV bindingflags.provider configResult
'workos' (default)KVCacheFlagProvider(WorkOSFlagProvider) + KV write store
'workos' (default)WorkOSFlagProvider (no writes)
anyKVFlagProvider + KV write store
anyWarning logged; all flags throw

Provider Classes

WorkOSFlagProvider

new WorkOSFlagProvider(apiKey: string)

Calls workos.featureFlags.getFeatureFlag(slug) and returns flag.enabled. Targeting rules (which users/orgs see the flag) are configured in the WorkOS dashboard.

KVCacheFlagProvider

new KVCacheFlagProvider(inner: FlagProvider, kv: KVNamespace, ttlSeconds?: number)

Wraps any FlagProvider with KV-backed edge caching. Default TTL is 60 seconds. Cache keys include the flag slug plus any userId and organizationId from context, so each targeted subject has an independent cache entry.

KVFlagProvider

new KVFlagProvider(kv: KVNamespace)

Reads/writes flags stored directly in KV as JSON. Suitable for infrastructure flags that don't need WorkOS dashboard management (maintenance mode, kill switches). Supports set().

Types

type FlagValue = boolean | number | string | Record<string, unknown>;

interface FlagContext {
  userId?: string;
  organizationId?: string;
  [key: string]: unknown;
}

interface FlagProvider {
  evaluate(key: string, context?: FlagContext): Promise<FlagValue>;
}

interface FlagStore {
  get<T = FlagValue>(flag: string): Promise<T | null>;
  set<T = FlagValue>(flag: string, value: T): Promise<void>;
}

Errors

FlagStoreNotConfiguredError

Thrown by isEnabled(), active(), getValue(), value(), and set() when no store or provider has been configured and no fake is active.

FlagNotFoundError

Thrown by getValue() and value() when the flag key does not exist in the store/provider.

Testing

FeatureFlagFake

Internal fake state managed by FeatureFlag.fake(). Tracks which flags were checked so assertChecked() can verify access.

class FeatureFlagFake {
  get<T extends FlagValue>(flag: string): Promise<T | null>;
  set<T extends FlagValue>(flag: string, value: T): Promise<void>;
  wasChecked(flag: string): boolean;
}

Cache Utilities

Used internally by FeatureFlagMiddleware and FeatureFlag.isEnabled(). Exposed for advanced use cases.

getRequestCache(request: Request): Map<string, FlagValue> | null

Read the per-request flag cache attached to request. Returns null if no cache has been set.

setRequestCache(request: Request, cache: Map<string, FlagValue>): void

Attach a flag cache map to request. The cache is keyed by FLAG_CACHE_KEY (a unique symbol) to avoid namespace collisions.

FLAG_CACHE_KEY: unique symbol

The symbol used to attach the cache to the request object.