@roostjs/feature-flags

Why WorkOS is the primary feature flag provider, the KV edge-cache model, the Pennant-style API design, and when to use flags vs config vs environment variables.

WorkOS as the Primary Provider

Feature flags need centralized management, user/org targeting, and fast reads at the edge. Roost uses WorkOS as the primary provider for flag definitions and evaluation, with Cloudflare KV as an optional edge-cache layer.

WorkOS provides a dashboard for managing flags — creating them, enabling/disabling them globally, and targeting specific users or organizations. The Roost SDK calls workos.featureFlags.getFeatureFlag(slug) to evaluate a flag at request time.

The KV Edge-Cache Layer

Calling the WorkOS API on every request adds network latency. KVCacheFlagProvider wraps WorkOSFlagProvider and caches evaluation results in Cloudflare KV with a configurable TTL (default 60 seconds). On a cache hit the flag is served from the nearest edge node without a round-trip to WorkOS.

Cache keys encode the flag slug plus any user/org context, so different users receive their own cached values independently.

When both WORKOS_API_KEY and a FLAGS_KV binding are present, FeatureFlagServiceProvider automatically composes WorkOSFlagProvider inside KVCacheFlagProvider. No extra configuration is needed.

The Per-Request Cache

Even with KV's edge caching, calling FeatureFlag.isEnabled() for the same flag multiple times in one request is wasteful. FeatureFlagMiddleware provides a per-request in-memory cache.

At the start of each request the middleware prefetches all configured flags in parallel and attaches the results to the request object under a unique symbol. Any call to isEnabled(flag, request) or getValue(flag, request) that passes the request reads from this cache without another KV or WorkOS round-trip.

The cache is scoped to the request lifetime. There are no cross-request invalidation concerns.

Pennant-Style API

FeatureFlag's static API is inspired by Laravel Pennant. The isEnabled() / active() / getValue() / value() / for() interface is intentionally minimal: one place to check a flag, one place to write it, one place to configure the provider.

The static API means there is no flag client instance to thread through the application. Any file that imports FeatureFlag or Feature can check a flag. The test helpers (fake(), restore(), assertChecked()) address the testing concern that globals usually introduce.

Feature is an alias for FeatureFlag — both names work.

Provider Architecture

Three provider types are available:

FeatureFlagServiceProvider selects a provider automatically:

WORKOS_API_KEYFLAGS_KV bindingActive provider
KVCacheFlagProvider(WorkOSFlagProvider)
WorkOSFlagProvider
KVFlagProvider
None (warns; all flags throw)

Override with flags.provider: 'kv' in app config to force KV-only mode even when WORKOS_API_KEY is present.

Flags vs Config vs Environment Variables

Environment variables are set at deploy time. Use them for values that require a deployment to update: API keys, external service URLs, resource limits.

App config (app.config.get(...)) layers over environment variables with structured defaults. Use it for values that vary between environments but are set at deploy time.

Feature flags are for values that need to change without a deployment: A/B tests, gradual rollouts, kill switches, beta feature gates. A developer deploys the flag check; an operator flips the flag in the WorkOS dashboard.

Truthy Evaluation Rules

FeatureFlag.isEnabled() and FeatureFlag.active() convert any FlagValue to a boolean:

isEnabled() is safe to call before a flag is created in WorkOS — it returns false rather than throwing.

Further Reading