# @terreno/feature-flags Feature flags and A/B testing plugin for `@terreno/api`. Provides a Mongoose model, admin CRUD endpoints, user-facing evaluation, and deterministic hashing for gradual rollouts and variant assignment. ## Install ```bash bun add @terreno/feature-flags ``` Peer dependency: `mongoose >= 8.0.0`. ## Quick Start ### Backend Register `FeatureFlagsApp` as a `TerrenoPlugin` and add the pre-configured admin model config to your `AdminApp`: ```typescript import {TerrenoApp} from "@terreno/api"; import {AdminApp} from "@terreno/admin-backend"; import {FeatureFlagsApp, featureFlagAdminConfig} from "@terreno/feature-flags"; import {User} from "./models/user"; const segments = { "pro-users": (user) => user.plan === "pro", "beta-testers": (user) => user.betaTester === true, }; new TerrenoApp({userModel: User}) .register(new FeatureFlagsApp({segments})) .register( new AdminApp({ models: [featureFlagAdminConfig], }) ) .start(); ``` `featureFlagAdminConfig` is a ready-to-use admin model config that sets up the display name, list fields, route path, and model reference — no manual configuration needed. Downstream consumers can import it directly and pass it to `AdminApp`. ### Frontend Use the `useFeatureFlags` hook from `@terreno/rtk` to evaluate flags for the current user: ```typescript import {useFeatureFlags} from "@terreno/rtk"; import {terrenoApi} from "@/store/sdk"; const MyComponent = () => { const {getFlag, getVariant, isLoading} = useFeatureFlags(terrenoApi); if (isLoading) return ; const showNewFeature = getFlag("new-feature"); // boolean const variant = getVariant("checkout-experiment"); // string | null return showNewFeature ? : ; }; ``` New code can also use OpenFeature React hooks (`useBooleanFlagValue`, `useStringFlagValue`, `FeatureFlag`, …) after wiring `useTerrenoFeatureFlags` and `` — see `@terreno/rtk` and [how-to: add feature flags](../how-to/add-feature-flags.md). ## OpenFeature integration `FeatureFlagsApp` registers an OpenFeature **server** provider (`MongoFeatureFlagProvider`) on a dedicated domain (default `"feature-flags"`) so the global default provider is unchanged. The authenticated bulk endpoint **`GET {basePath}/flagConfiguration`** returns a `FlagConfiguration`-compatible map (suitable for `TypedInMemoryProvider` on the client). The legacy **`GET {basePath}/evaluate`** response shape is unchanged but is **deprecated**: responses include `Deprecation: true` and a `Sunset` HTTP-date header (~90 days). Prefer `useTerrenoFeatureFlags` / `useFeatureFlags` from `@terreno/rtk`, or migrate direct HTTP callers to `/flagConfiguration`. ### `defaultVariant` on `FeatureFlag` Stored OpenFeature default variant key: for boolean flags `"on"` or `"off"`; for variant flags one of `variants[].key`. On save, if omitted, the schema sets boolean → `"off"` and variant → first variant key. `/flagConfiguration` builds each entry’s `defaultVariant` and `variants` from the **resolved** value for the current user (so client-side OpenFeature reads the correct branch without re-running targeting). ### `MongoFeatureFlagProvider` Server-side OpenFeature `Provider` that resolves flags via `findOneOrNone` and existing `evaluateFlag()` logic. Unsupported evaluation types: **`resolveNumberEvaluation` / `resolveObjectEvaluation` always return `FLAG_NOT_FOUND`** with the caller’s default. ### Type-safe flag keys (optional) Consumers may augment `@openfeature/core` with `BooleanFlagKey` / `StringFlagKey` unions. Those types are **compile-time hints only**; keys created in the admin UI can drift until types are updated. ### Live updates (optional) Pass `liveUpdates: { socketIoServer: io | () => io }` to broadcast on Mongoose change streams. **Requires MongoDB as a replica set** (single-node replset is fine). Emits a socket event (default `featureFlagsChanged`, payload `{ key }`) and `PROVIDER_CONFIGURATION_CHANGED` on the server provider. All authenticated subscribers receive the same event name; payload is only the flag key (same keys are already exposed per user via `/flagConfiguration`). ## Exports ### Classes | Export | Description | |--------|-------------| | `FeatureFlagsApp` | `TerrenoPlugin` that registers admin CRUD, `/flagConfiguration`, and `/evaluate` | | `MongoFeatureFlagProvider` | OpenFeature server `Provider` backed by Mongo evaluation | ### Models | Export | Description | |--------|-------------| | `FeatureFlag` | Mongoose model for feature flag documents | ### Constants | Export | Description | |--------|-------------| | `featureFlagAdminConfig` | Pre-configured `AdminModelConfig` for use with `AdminApp` | ### Functions | Export | Description | |--------|-------------| | `buildFlagDefinition` | Build one `FlagDefinition` for `/flagConfiguration` | | `effectiveDefaultVariantForFlag` | Resolve `defaultVariant` including legacy docs | | `evaluateFlag` | Evaluate a single flag for a user | | `evaluateAllFlags` | Evaluate all enabled, non-archived flags for a user | | `deterministicHash` | Hash a string to 0–99 for consistent assignment | ### Types | Export | Description | |--------|-------------| | `FeatureFlagDocument` | Mongoose document type for a feature flag | | `FeatureFlagModel` | Mongoose model type with custom statics | | `FeatureFlagRule` | Targeting rule shape (field-based or segment-based) | | `FeatureFlagVariant` | Variant key + weight for A/B tests | | `FeatureFlagType` | `"boolean" \| "variant"` | | `FeatureFlagsOptions` | Constructor options for `FeatureFlagsApp` | | `SegmentFunction` | `(user: unknown) => boolean` | | `EvaluationResult` | `Record` | | `FlagDefinition` | One flag entry for `/flagConfiguration` | | `FlagConfigurationResponse` | `{ data: Record }` | | `FeatureFlagsLiveUpdatesOptions` | Socket.io + optional custom event name | | `FeatureFlagsSocketEmitter` | Minimal `emit` shape for live updates | ## FeatureFlagsApp Options ```typescript interface FeatureFlagsOptions { basePath?: string; // Default: "/feature-flags" segments?: Record; // Named segment functions permissions?: ModelRouterOptions["permissions"]; // Override default IsAdmin on CRUD segmentsPermission?: (user: unknown) => boolean; // Override admin check on /segments liveUpdates?: FeatureFlagsLiveUpdatesOptions; // Optional: change stream → socket.io openFeatureDomain?: string; // Default: "feature-flags" } ``` ## Generated Routes All routes are mounted under the configured `basePath` (default: `/feature-flags`). ### Admin Routes (default: `IsAdmin`) | Method | Path | Description | |--------|------|-------------| | `POST` | `{basePath}/flags/` | Create a feature flag | | `GET` | `{basePath}/flags/` | List flags (paginated, sortable) | | `GET` | `{basePath}/flags/:id` | Get a single flag | | `PATCH` | `{basePath}/flags/:id` | Update a flag | | `DELETE` | `{basePath}/flags/:id` | Soft-delete a flag | | `GET` | `{basePath}/segments` | List registered segment names | ### User Routes (`IsAuthenticated`) | Method | Path | Description | |--------|------|-------------| | `GET` | `{basePath}/flagConfiguration` | OpenFeature static configuration for enabled, non-archived flags (authenticated) | | `GET` | `{basePath}/evaluate` | **Deprecated** — legacy `Record` for the current user | ### Response Shapes **`GET /feature-flags/flagConfiguration`** (enabled flags only; disabled/archived keys are omitted): ```json { "data": { "todo-summary-card": { "variants": {"off": false, "on": true}, "defaultVariant": "on", "disabled": false }, "profile-layout": { "variants": {"compact": "compact", "detailed": "detailed"}, "defaultVariant": "compact", "disabled": false } } } ``` **`GET /feature-flags/evaluate`** (deprecated headers on response): ```json { "data": { "new-checkout-flow": true, "checkout-experiment": "variant-a", "dark-mode": false } } ``` **`GET /feature-flags/segments`**: ```json { "data": ["pro-users", "beta-testers"] } ``` ## Flag Types ### Boolean Flags (`type: "boolean"`) Return `true` or `false`. Use `rolloutPercentage` for gradual rollouts. - `enabled: false` → always `false` - `enabled: true`, no matching rules → deterministic hash compared against `rolloutPercentage` - Matching rule → returns the rule's `enabled` value ### Variant Flags (`type: "variant"`) Return a string variant key. Use the `variants` array with weighted distribution. - `enabled: false` → returns `null` - `enabled: true`, no matching rules → deterministic hash mapped to variant by cumulative weights - Matching rule → returns the rule's `variant` value ## Targeting Rules Rules are evaluated in order — first match wins. Each rule is either field-based or segment-based. ### Field Rules Match against a user field with an operator: ```json { "field": "email", "operator": "contains", "value": "@mycompany.com", "enabled": true } ``` Supported operators: `eq`, `neq`, `in`, `nin`, `gt`, `lt`, `contains`. Dot notation is supported for nested fields (e.g. `address.zip`). ### Segment Rules Match against a named segment function registered at startup: ```json { "segment": "pro-users", "enabled": true } ``` ## Admin Integration The `featureFlagAdminConfig` export provides everything `AdminApp` needs: ```typescript import {featureFlagAdminConfig} from "@terreno/feature-flags"; // Use directly in AdminApp new AdminApp({ models: [featureFlagAdminConfig, ...otherModels], }); ``` This is equivalent to manually configuring: ```typescript import {FeatureFlag} from "@terreno/feature-flags"; { displayName: "Feature Flags", listFields: ["key", "name", "type", "enabled", "archived", "defaultVariant", "created"], model: FeatureFlag, routePath: "/feature-flags", } ``` The admin panel auto-generates forms from the FeatureFlag model schema, including fields for rules, variants, rollout percentage, and archiving. ## Segments Segments are named functions that classify users. Register them when constructing `FeatureFlagsApp`: ```typescript const segments = { "admin-users": (user) => user.admin === true, "oauth-users": (user) => Boolean(user.oauthProvider), "high-usage": (user) => user.totalActions > 1000, "internal-team": (user) => user.email?.endsWith("@mycompany.com"), }; new FeatureFlagsApp({segments}); ``` Segment names can be referenced in flag rules via the admin UI. ## Environment Variables | Variable | Description | Default | |----------|-------------|---------| | `FEATURE_FLAGS_DEBUG` | Set to `"false"` to disable evaluation debug logs | Enabled |