@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().