|$ curl https://forge-ai.dev/api/markdown?path=docs/deploy/feature-flags
$cat docs/deploy-—-feature-flags-&-progressive-delivery.md
updated Recently·14 min read·published

Deploy — Feature Flags & Progressive Delivery

CI/CDIntermediate
Introduction

Feature flags (also called feature toggles or feature switches) decouple code deployment from feature release. They let you deploy code to production and enable features selectively — to specific users, cohorts, or percentages — without a new deployment.

This section covers flag types, management platforms (LaunchDarkly, Unleash, Flagsmith), the OpenFeature standard, canary and A/B testing patterns, and SDK integration for React and Node.js.

What are Feature Flags?

A feature flag is a boolean or configuration value that controls whether a piece of code is executed. At its simplest, it is an if-statement backed by an external configuration source that can be changed at runtime without redeploying.

flag-usage.ts
TypeScript
1// Simple boolean flag
2const enableNewCheckout = flags.isEnabled("new-checkout-flow");
3
4if (enableNewCheckout) {
5 return renderNewCheckout(props);
6} else {
7 return renderLegacyCheckout(props);
8}
9
10// Flag with default value (fail-open)
11const darkMode = flags.isEnabled("dark-mode", { defaultValue: false });
12
13// Flag targeting specific users
14const betaFeature = flags.isEnabled("beta-feature", {
15 userId: user.id,
16 attributes: {
17 plan: user.plan,
18 region: user.region,
19 },
20});

info

Feature flags turn deployment into a two-step process: deploy the code (hidden behind a flag) and then release the feature (toggle the flag). This eliminates the risk of shipping untested features to all users at once.
Types of Flags

Not all flags serve the same purpose. Categorizing flags by their lifecycle and purpose helps manage complexity and prevents flag debt.

TypePurposeLifecycle
ReleaseGate new features during rolloutShort-lived (days/weeks)
ExperimentA/B test variations for optimizationMedium (weeks/months)
OpsKill switches, maintenance mode, rate limitingLong-lived (permanent)
PermissionControl access based on plan, role, or entitlementsLong-lived

warning

Always track which flags exist and their type. Release flags that are never removed accumulate "flag debt" — dead code paths that make the codebase harder to reason about. Schedule regular flag cleanup sprints.
Flag Management Platforms

Managed feature flag platforms provide dashboards, SDKs, audit logs, and targeting engines. Self-hosted options give you full control over flag data.

PlatformTypeBest For
LaunchDarklyManaged SaaSEnterprise teams needing advanced targeting, audit, and compliance
UnleashOpen-source / ManagedTeams wanting self-hosted with full control
FlagsmithOpen-source / ManagedSimple, clean API with self-hosting option
OpenFeatureVendor-neutral APIAvoiding vendor lock-in with a standard interface
OpenFeature Standard

OpenFeature is a vendor-neutral standard for feature flagging. It provides a unified API that works with any flag provider, so you can switch providers without changing application code.

openfeature.ts
TypeScript
1import { OpenFeature } from "@openfeature/js-sdk";
2
3// Initialize with a provider (LaunchDarkly, Unleash, etc.)
4await OpenFeature.setProvider(new YourFlagProvider({
5 apiKey: process.env.FLAG_API_KEY,
6}));
7
8const client = OpenFeature.getClient("my-app", "production");
9
10// Evaluate a boolean flag
11const showNewUI = await client.getBooleanValue("new-ui", false);
12
13// Evaluate a string flag (for variations)
14const checkoutVersion = await client.getStringValue(
15 "checkout-experiment",
16 "control",
17 {
18 targetingKey: user.id,
19 plan: user.plan,
20 }
21);
22
23// Evaluate a number flag (e.g., max items)
24const maxItems = await client.getNumberValue("max-cart-items", 10);

best practice

Adopting OpenFeature from the start prevents vendor lock-in. If your current flag provider becomes too expensive or loses a feature, you can switch providers by changing only the provider initialization — not the hundreds of flag evaluations throughout your code.
Canary Releases & Gradual Rollouts

Feature flags enable gradual rollouts — releasing features to a small percentage of users first, monitoring for errors, and expanding gradually. This is also known as canary releasing.

canary-release.ts
TypeScript
1// Gradual rollout: percentage-based targeting
2const config = {
3 flag: "new-search-algorithm",
4 rules: [
5 { percentage: 5, variation: "enabled" }, // 5% get the new feature
6 { percentage: 100, variation: "disabled" }, // rest get the old one
7 ],
8};
9
10// Stage-based rollout
11const stages = [
12 { percent: 1, label: "canary", duration: "2h" },
13 { percent: 10, label: "early", duration: "4h" },
14 { percent: 25, label: "beta", duration: "24h" },
15 { percentage: 100, label: "ga", duration: "permanent" },
16];
17
18// Monitor error rate at each stage before advancing
19async function advanceRollout(flagName: string, stage: number) {
20 const errorRate = await getErrorRate(flagName);
21 if (errorRate > 0.01) { // > 1% error rate
22 await disableFlag(flagName);
23 await alertOnCall("Rollback triggered for " + flagName);
24 return;
25 }
26 if (stage < stages.length - 1) {
27 await setFlagPercentage(flagName, stages[stage + 1].percent);
28 await scheduleAdvance(flagName, stage + 1, stages[stage].duration);
29 }
30}

info

Tie your rollout progression to observability. Automatically halt or rollback a canary if error rates, latency percentiles, or business metrics (conversion, revenue) degrade beyond a threshold.
A/B Testing Infrastructure

A/B testing uses feature flags with user segmentation to test variations of a feature and measure their impact on key metrics. Proper A/B testing requires consistent user bucketing and statistical rigor.

ab-testing.ts
TypeScript
1// A/B test: two variations of a pricing page
2const client = OpenFeature.getClient("my-app");
3
4async function getPricingPage(userId: string) {
5 const variation = await client.getStringValue(
6 "pricing-page-test",
7 "control",
8 { targetingKey: userId }
9 );
10
11 // Track exposure for analysis
12 trackExposure("pricing-page-test", variation, userId);
13
14 return variation === "control"
15 ? renderCurrentPricing()
16 : renderNewPricing();
17}
18
19// Analyze results after experiment concludes
20function analyzeResults(exposures: Exposure[]) {
21 const control = exposures.filter(e => e.variation === "control");
22 const treatment = exposures.filter(e => e.variation === "treatment");
23
24 const controlConversion = control.filter(e => e.converted).length / control.length;
25 const treatmentConversion = treatment.filter(e => e.converted).length / treatment.length;
26
27 const uplift = (treatmentConversion - controlConversion) / controlConversion;
28 const pValue = calculatePValue(control, treatment);
29
30 return { uplift, pValue, significant: pValue < 0.05 };
31}
Flag Lifecycle

Every flag should follow a clear lifecycle: create, evaluate, and remove. Flags that are never removed create maintenance burden and hide dead code.

PhaseActions
CreateDefine flag, set type (release/experiment/ops), document purpose and owner
EvaluateDeploy code with flag, enable gradually, monitor metrics
RemoveOnce fully rolled out, remove the flag and dead code, close the ticket

warning

Create a "flag debt" dashboard. List all active flags, their age, and their owner. Flags older than 30 days for release-type flags should trigger automated alerts.
SDK Integration

Feature flag SDKs are available for every major platform. Here is how to integrate flags in React and Node.js.

react-flags.tsx
TypeScript
1// React: Wrap app with OpenFeature provider
2"use client";
3import { OpenFeatureProvider, useFlag } from "@openfeature/js-sdk-react";
4
5function App() {
6 return (
7 <OpenFeatureProvider client={client}>
8 <Dashboard />
9 </OpenFeatureProvider>
10 );
11}
12
13// Use flags in components
14function Dashboard() {
15 const { value: showAnalytics } = useFlag("analytics-widget", false);
16 const { value: layout } = useFlag("dashboard-layout", "grid");
17
18 return (
19 <div className={layout}>
20 {showAnalytics && <AnalyticsWidget />}
21 <RecentActivity />
22 </div>
23 );
24}
node-flags.ts
TypeScript
1// Node.js: Server-side flag evaluation
2import { OpenFeature } from "@openfeature/js-sdk";
3
4const client = OpenFeature.getClient("api-server");
5
6app.get("/api/products", async (req, res) => {
7 const useNewQuery = await client.getBooleanValue(
8 "new-product-query",
9 false,
10 { targetingKey: req.user?.id ?? "anonymous" }
11 );
12
13 const products = useNewQuery
14 ? await fetchProductsWithNewQuery()
15 : await fetchProductsWithLegacyQuery();
16
17 res.json(products);
18});
Flag Naming & Organization

Consistent naming conventions make flags discoverable and self-documenting. Use a hierarchical naming scheme that encodes ownership and type.

flag-naming.ts
TypeScript
1// Convention: [team].[feature].[variant]
2// Examples:
3const flags = {
4 // Release flags
5 "payments.new-checkout-flow": true,
6 "search.v2-ranking-algorithm": false,
7
8 // Experiment flags
9 "growth.pricing-page-v2": "treatment",
10 "onboarding.tutorial-style": "interactive",
11
12 // Ops flags
13 "platform.maintenance-mode": false,
14 "api.rate-limit-enabled": true,
15
16 // Permission flags
17 "billing.export-csv": true, // plan-based
18 "admin.advanced-analytics": true, // role-based
19};

best practice

Prefix flags with team or domain ownership. When a flag triggers an incident, the naming convention immediately tells you who to page. Document every flag with its type, owner, and expiration date in your flag management platform.
Testing with Feature Flags

Testing code behind flags requires testing both the enabled and disabled paths. Mock the flag provider in tests to control which code paths execute.

flag-testing.ts
TypeScript
1// Jest: Mock flag provider for deterministic tests
2import { InMemoryProvider, OpenFeature } from "@openfeature/js-sdk";
3
4beforeEach(() => {
5 const provider = new InMemoryProvider({
6 "new-checkout-flow": { value: true },
7 "dark-mode": { value: false },
8 });
9 OpenFeature.setProvider(provider);
10});
11
12test("uses new checkout when flag is enabled", async () => {
13 const client = OpenFeature.getClient("test");
14 const result = await client.getBooleanValue("new-checkout-flow", false);
15 expect(result).toBe(true);
16});
17
18test("uses legacy checkout when flag is disabled", async () => {
19 const provider = new InMemoryProvider({
20 "new-checkout-flow": { value: false },
21 });
22 OpenFeature.setProvider(provider);
23
24 const client = OpenFeature.getClient("test");
25 const result = await client.getBooleanValue("new-checkout-flow", false);
26 expect(result).toBe(false);
27});
Best Practices

✓ Limit flag scope

Use flags for small, well-defined features. Avoid flags that control large swaths of code — they become impossible to test comprehensively.

✓ Remove flags after full rollout

Set a deadline for flag removal when you create it. Use automated reminders and cleanup sprints to prevent flag debt.

✓ Use flag defaults for offline resilience

Always specify a safe default value. If the flag provider is unreachable, the application should degrade gracefully to the default state.

✓ Test both flag states

Every flag creates a new code path. Write tests for both the enabled and disabled states to ensure neither path is broken.

$Blueprint — Engineering Documentation·Section ID: CICD-FLAGS·Revision: 1.0