> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userefract.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Feature Flags Overview

> Provider-agnostic feature flag evaluation with request and shared caching.

**Feature flags** give backend and portal code a single `tools.featureFlags` contract while PostHog, LaunchDarkly, or a local dev stub handle evaluation behind the scenes.

<details>
  <summary>🤖 Agent context</summary>

  * Contract + schema: `apps/shared/src/featureFlags/` (`FeatureFlagTool`, `FeatureFlagProviderType`, `featureFlagConfigSchema`, `buildFeatureFlagEvaluationKey`)
  * Config type: `apps/backend/src/configuration/type.ts` → `tools.featureFlags`
  * Loader + caching: `apps/backend/src/tools/featureFlags/loader.ts`, `cachedFeatureFlags.ts`, `requestCache.ts`
  * L2 Redis helper: `apps/backend/src/tools/cache/featureFlagEvaluation.ts` → `tools.cache.helpers.featureFlagEvaluation` (adapted via `sharedCacheAdapter.ts` → `FeatureFlagSharedCache` port)
  * Implementations: `tooling-feature-flags-local`, `tooling-feature-flags-posthog`, `tooling-feature-flags-launch-darkly` under `apps/tools/featureFlags/`
  * GraphQL exposure: `me.featureFlags` in `apps/backend/src/gql/queries/me.ts` — only keys listed in `tools.featureFlags.exposedFlags`
  * Request cache middleware: `featureFlagRequestCacheMiddleware` in `apps/backend/src/index.ts`

  ❌ Do not call PostHog or LaunchDarkly SDKs outside `apps/tools/featureFlags/<impl>/`.
  ❌ Do not expose internal operational flags through `exposedFlags`.
  ❌ Do not bypass `tools.featureFlags` from GraphQL resolvers or utilities.

  Verify with: `make test module=backend path=apps/backend/src/tools/featureFlags` and `make test module=tooling-feature-flags-local`
</details>

## Why

Feature flags belong in infrastructure, not scattered `if (process.env…)` checks. Refract normalizes every provider into the same evaluation model (`enabled` + optional `variant`), adds request-level consistency for GraphQL, and optionally shares results across requests via `cacheMs`.

## Public API

```ts theme={null}
await tools.featureFlags.preload(context);
await tools.featureFlags.isEnabled('new-dashboard', context);
await tools.featureFlags.getVariant('checkout-experiment', context);
await tools.featureFlags.getAll(context);
```

`preload()` is transparent — `isEnabled`, `getVariant`, and `getAll` call it automatically. Multiple reads in the same HTTP request hit the provider at most once when bulk caching succeeds.

## Configuration

```ts theme={null}
featureFlags: {
  client: FeatureFlagClientType.LOCAL,
  cacheMs: 30_000,
  exposedFlags: ['new-dashboard'],
}
```

* `cacheMs: 0` disables the shared cache; request caching remains enabled.
* `exposedFlags` controls which keys appear on `me.featureFlags`.

## Provider contract

Remote packages implement **`FeatureFlagProviderType`**: the same surface as `FeatureFlagTool` except `preload` (the backend cache wrapper owns preload).

| Method | Provider responsibility |
| - | - |
| `getAll(context)` | Bulk evaluation when the vendor supports it (PostHog `getAllFlags`, LaunchDarkly `allFlagsState`). May return `{}` when bulk is unavailable. |
| `isEnabled(key, context)` | Per-flag boolean evaluation — used on cache miss and required for every provider. |
| `getVariant(key, context)` | Per-flag variant evaluation — same fall-through rules as `isEnabled`. |
| `shutdown()` | Optional SDK cleanup. |

**LOCAL** returns a full `FeatureFlagTool` directly (no cache wrapper). **PostHog** and **LaunchDarkly** return a provider; `buildCachedFeatureFlagTool` in the loader adds `preload` plus L1/L2 caching.

When adding a provider, implement all three evaluation methods. Bulk-first vendors should make `getAll` efficient; per-flag-only vendors can no-op `getAll` to `{}` and rely on fall-through (see below).

## Caching

Resolution order per evaluation context key (`user:123|org:456`, `anonymous`, etc.):

### Bulk path (`preload` / `getAll`)

1. Request cache (always on for HTTP traffic via AsyncLocalStorage)
2. Shared Redis cache (`tools.cache.helpers.featureFlagEvaluation`, TTL from `cacheMs`) — accessed through the `FeatureFlagSharedCache` port
3. Provider `getAll` (vendor bulk API)

### Per-flag path (`isEnabled` / `getVariant`)

1. Same request + shared bulk snapshot when the key **is present** in the cached map (including `enabled: false`)
2. **Fall-through** to provider `isEnabled` / `getVariant` when:
   * the key is **absent** from the bulk snapshot (e.g. provider returned `{}`, or bulk omitted that flag)
   * bulk population failed (errors are swallowed; per-flag calls still work)

Fall-through does **not** run when the bulk snapshot explicitly contains the key — a cached `{ enabled: false }` is authoritative.

```mermaid theme={null}
flowchart TD
  call[isEnabled / getVariant]
  preload[preload → getAll bulk path]
  cache{Key in bulk snapshot?}
  cached[Return cached value]
  api[Provider isEnabled / getVariant]

  call --> preload --> cache
  cache -->|yes| cached
  cache -->|no| api
```

The L2 helper is registered in `apps/backend/src/tools/cache/featureFlagEvaluation.ts` and wired when `buildFeatureFlags` runs with `tools.cache`.

## GraphQL

Authenticated clients can read frontend-safe flags from `me.featureFlags`. Internal flags stay server-side and are accessed only through `tools.featureFlags`.

Evaluation context is built from the authenticated user (`userId`, current organization from membership). If `user.memberships` is not hydrated, the utility loads them from the database before resolving flags.

## Providers

| Provider | Package | Use when |
| - | - | - |
| Local | `tooling-feature-flags-local` | Development and Jest (`configuration/test.ts`) |
| PostHog | `tooling-feature-flags-posthog` | You already run PostHog for analytics |
| LaunchDarkly | `tooling-feature-flags-launch-darkly` | Mature flag workflows and enterprise targeting |

## What's next?

* [Local provider](/tooling/feature-flags/local)
* [PostHog provider](/tooling/feature-flags/posthog)
* [LaunchDarkly provider](/tooling/feature-flags/launch-darkly)
* [Tooling system](/architecture/tooling-system)
* [Configuration system](/architecture/configuration-system)
