Skip to main content
Version: 57.2.0

@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

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:

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:

import {useFeatureFlags} from "@terreno/rtk";
import {terrenoApi} from "@/store/sdk";

const MyComponent = () => {
const {getFlag, getVariant, isLoading} = useFeatureFlags(terrenoApi);

if (isLoading) return <Spinner />;

const showNewFeature = getFlag("new-feature"); // boolean
const variant = getVariant("checkout-experiment"); // string | null

return showNewFeature ? <NewFeature /> : <OldFeature />;
};

New code can also use OpenFeature React hooks (useBooleanFlagValue, useStringFlagValue, FeatureFlag, …) after wiring useTerrenoFeatureFlags and <OpenFeatureProvider domain="feature-flags"> — see @terreno/rtk and how-to: add feature flags.

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

ExportDescription
FeatureFlagsAppTerrenoPlugin that registers admin CRUD, /flagConfiguration, and /evaluate
MongoFeatureFlagProviderOpenFeature server Provider backed by Mongo evaluation

Models

ExportDescription
FeatureFlagMongoose model for feature flag documents

Constants

ExportDescription
featureFlagAdminConfigPre-configured AdminModelConfig for use with AdminApp

Functions

ExportDescription
buildFlagDefinitionBuild one FlagDefinition for /flagConfiguration
effectiveDefaultVariantForFlagResolve defaultVariant including legacy docs
evaluateFlagEvaluate a single flag for a user
evaluateAllFlagsEvaluate all enabled, non-archived flags for a user
deterministicHashHash a string to 0–99 for consistent assignment

Types

ExportDescription
FeatureFlagDocumentMongoose document type for a feature flag
FeatureFlagModelMongoose model type with custom statics
FeatureFlagRuleTargeting rule shape (field-based or segment-based)
FeatureFlagVariantVariant key + weight for A/B tests
FeatureFlagType"boolean" | "variant"
FeatureFlagsOptionsConstructor options for FeatureFlagsApp
SegmentFunction(user: unknown) => boolean
EvaluationResultRecord<string, boolean | string | null>
FlagDefinitionOne flag entry for /flagConfiguration
FlagConfigurationResponse{ data: Record<string, FlagDefinition> }
FeatureFlagsLiveUpdatesOptionsSocket.io + optional custom event name
FeatureFlagsSocketEmitterMinimal emit shape for live updates

FeatureFlagsApp Options

interface FeatureFlagsOptions {
basePath?: string; // Default: "/feature-flags"
segments?: Record<string, SegmentFunction>; // 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)

MethodPathDescription
POST{basePath}/flags/Create a feature flag
GET{basePath}/flags/List flags (paginated, sortable)
GET{basePath}/flags/:idGet a single flag
PATCH{basePath}/flags/:idUpdate a flag
DELETE{basePath}/flags/:idSoft-delete a flag
GET{basePath}/segmentsList registered segment names

User Routes (IsAuthenticated)

MethodPathDescription
GET{basePath}/flagConfigurationOpenFeature static configuration for enabled, non-archived flags (authenticated)
GET{basePath}/evaluateDeprecated — legacy Record<key, boolean | string | null> for the current user

Response Shapes

GET /feature-flags/flagConfiguration (enabled flags only; disabled/archived keys are omitted):

{
"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):

{
"data": {
"new-checkout-flow": true,
"checkout-experiment": "variant-a",
"dark-mode": false
}
}

GET /feature-flags/segments:

{
"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:

{
"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:

{
"segment": "pro-users",
"enabled": true
}

Admin Integration

The featureFlagAdminConfig export provides everything AdminApp needs:

import {featureFlagAdminConfig} from "@terreno/feature-flags";

// Use directly in AdminApp
new AdminApp({
models: [featureFlagAdminConfig, ...otherModels],
});

This is equivalent to manually configuring:

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:

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

VariableDescriptionDefault
FEATURE_FLAGS_DEBUGSet to "false" to disable evaluation debug logsEnabled