@roostjs/feature-flags Guides
Task-oriented instructions for defining flags in WorkOS, checking them in handlers, scoped user/org evaluation, prefetching with middleware, and testing flag-gated behavior.
How to register the feature flag provider
Add FeatureFlagServiceProvider to your app's provider list. With WORKOS_API_KEY in env and a FLAGS_KV binding, it automatically uses WorkOS for evaluation with KV edge caching.
import { createApp } from '@roostjs/core';
import { FeatureFlagServiceProvider } from '@roostjs/feature-flags';
export const app = createApp({
providers: [
new FeatureFlagServiceProvider(),
// ...other providers
],
});To use KV-only mode (no WorkOS), set flags.provider in config:
export const app = createApp({
config: { flags: { provider: 'kv', kv: 'FLAGS_KV' } },
providers: [new FeatureFlagServiceProvider()],
});How to check a feature flag in a route handler
Call FeatureFlag.isEnabled() or FeatureFlag.active() anywhere after the provider has registered. Both are identical — use whichever reads more naturally.
import { FeatureFlag } from '@roostjs/feature-flags';
export async function handleCheckout(request: Request): Promise<Response> {
const newCheckoutEnabled = await FeatureFlag.active('new-checkout');
if (newCheckoutEnabled) {
return handleNewCheckout(request);
}
return handleLegacyCheckout(request);
}How to evaluate a flag for a specific user or organization
Use FeatureFlag.for(context) to create a scoped evaluator that passes user/org context to the provider. WorkOS uses this context when evaluating targeting rules configured in the dashboard.
import { FeatureFlag } from '@roostjs/feature-flags';
export async function handleDashboard(request: Request, userId: string, orgId: string) {
const scoped = FeatureFlag.for({ userId, organizationId: orgId });
if (await scoped.active('new-dashboard')) {
return renderNewDashboard(request);
}
return renderLegacyDashboard(request);
}How to read a typed flag value
Use value<T>() on a scoped evaluator, or getValue<T>() on the static class, when you need the raw value rather than a boolean.
import { FeatureFlag } from '@roostjs/feature-flags';
// With a default value (never throws)
const limit = await FeatureFlag.value<number>('api-rate-limit', 100);
// Scoped to a user
const color = await FeatureFlag.for({ userId }).value<string>('button-color', 'blue');getValue() and value() throw FlagNotFoundError if the key is missing and no default is provided.
How to prefetch flags for a request
Use FeatureFlagMiddleware to batch-fetch flags at the start of each request. Downstream calls to isEnabled(flag, request) or active(flag, request) read from the per-request cache rather than hitting the provider again.
import { createApp } from '@roostjs/core';
import { FeatureFlagMiddleware, FeatureFlagServiceProvider } from '@roostjs/feature-flags';
export const app = createApp({
providers: [new FeatureFlagServiceProvider()],
middleware: [
new FeatureFlagMiddleware(['new-checkout', 'beta-dashboard', 'api-rate-limit']),
],
});Then pass request when checking flags to read from the cache:
const enabled = await FeatureFlag.active('new-checkout', request);How to write a flag value
Use FeatureFlag.set() to update a flag programmatically — from an admin panel, a seed script, or a management endpoint. Requires a KV binding to be configured.
import { FeatureFlag } from '@roostjs/feature-flags';
await FeatureFlag.set('new-checkout', true);
await FeatureFlag.set('api-rate-limit', 500);
await FeatureFlag.set('checkout-config', { maxItems: 50, allowGuestCheckout: true });For toggling flags in production, prefer the WorkOS dashboard — set() writes to the KV cache layer, not the WorkOS flag definition itself.
How to test flag-gated behavior
Call FeatureFlag.fake() with the desired flag values before running the code under test. Use FeatureFlag.assertChecked() to verify the flag was actually consulted.
import { describe, it, afterEach } from 'bun:test';
import { FeatureFlag } from '@roostjs/feature-flags';
import { handleCheckout } from '../../src/handlers/billing';
describe('handleCheckout', () => {
afterEach(() => FeatureFlag.restore());
it('uses the new checkout when flag is enabled', async () => {
FeatureFlag.fake({ 'new-checkout': true });
const response = await handleCheckout(new Request('https://app/checkout'));
FeatureFlag.assertChecked('new-checkout');
});
it('falls back to legacy checkout when flag is disabled', async () => {
FeatureFlag.fake({ 'new-checkout': false });
const response = await handleCheckout(new Request('https://app/checkout'));
FeatureFlag.assertChecked('new-checkout');
});
});FeatureFlag.fake() intercepts all evaluation including FeatureFlag.for(context).active() — the context is ignored in fake mode, which keeps tests simple.
Flags not in the fake map return null from getValue()/value() and false from isEnabled()/active().