Claude Code for Feature Flags: OpenFeature, LaunchDarkly, and Safe Rollouts — Claude Skills 360 Blog
Blog / Architecture / Claude Code for Feature Flags: OpenFeature, LaunchDarkly, and Safe Rollouts
Architecture

Claude Code for Feature Flags: OpenFeature, LaunchDarkly, and Safe Rollouts

Published: October 4, 2026
Read time: 8 min read
By: Claude Skills 360

Feature flags decouple deployment from release. Code ships to production disabled, then gradually turns on for 1%, 10%, 100% of users — or specific cohorts — without code changes or redeployments. Claude Code implements the OpenFeature standard so your flag evaluation code is provider-agnostic, integrates with LaunchDarkly or custom backends, and wraps database migrations and risky features in flags that can be disabled instantly.

CLAUDE.md for Feature Flag Projects

## Feature Flags
- Standard: OpenFeature SDK (@openfeature/server-sdk)
- Provider: LaunchDarkly (production), in-memory JSON file (local dev/tests)
- Flag naming: snake_case, descriptive — e.g., checkout_v2_enabled, new_search_algorithm
- Every flag must have: description, owner, created date, removal date
- Flag types: boolean (on/off), string (variant), number (percentage), object (config)
- Never hardcode flag keys — import from src/flags.ts constants file
- All flags start as false (off by default)
- Remove flags within 30 days of full rollout

OpenFeature Setup

// src/flags.ts — centralized flag key registry
export const FLAGS = {
  CHECKOUT_V2: 'checkout_v2_enabled',
  NEW_SEARCH: 'new_search_algorithm',
  PRODUCT_RECOMMENDATIONS: 'product_recommendations_enabled',
  FREE_SHIPPING_THRESHOLD: 'free_shipping_threshold_cents',  // number flag
} as const;

// src/lib/featureFlags.ts — OpenFeature initialization
import { OpenFeature } from '@openfeature/server-sdk';
import { LaunchDarklyProvider } from '@launchdarkly/openfeature-node-server';
import { InMemoryProvider } from '@openfeature/in-memory-provider';

export async function initFeatureFlags() {
  if (process.env.NODE_ENV === 'production') {
    const provider = new LaunchDarklyProvider(process.env.LAUNCHDARKLY_SDK_KEY!);
    await OpenFeature.setProviderAndWait(provider);
  } else {
    // Local dev: read from flags.json, no network required
    const flagConfig = await import('./flags-local.json');
    const provider = new InMemoryProvider(flagConfig.flags);
    await OpenFeature.setProviderAndWait(provider);
  }
}

// flags-local.json — local development defaults
// {
//   "flags": {
//     "checkout_v2_enabled": { "defaultVariant": "off", "variants": { "on": true, "off": false } },
//     "free_shipping_threshold_cents": { "defaultVariant": "default", "variants": { "default": 5000 } }
//   }
// }

Evaluating Flags

// src/lib/featureFlags.ts (continued)
import { OpenFeature, type EvaluationContext } from '@openfeature/server-sdk';

// Evaluation context: who is making the request?
// Pass this for targeting rules (user tier, region, beta access)
export function buildContext(user: User): EvaluationContext {
  return {
    targetingKey: user.id,
    email: user.email,
    plan: user.plan,         // 'free' | 'pro' | 'enterprise'
    country: user.country,
    betaUser: user.betaAccess,
  };
}

// Typed flag evaluation helpers
const client = OpenFeature.getClient();

export async function isEnabled(
  flagKey: string,
  context: EvaluationContext,
  defaultValue = false,
): Promise<boolean> {
  const result = await client.getBooleanDetails(flagKey, defaultValue, context);
  
  // Log evaluation for debugging (not in hot paths)
  if (process.env.FLAG_DEBUG === '1') {
    console.log(`Flag [${flagKey}]: ${result.value} (reason: ${result.reason})`);
  }
  
  return result.value;
}

export async function getStringVariant(
  flagKey: string,
  context: EvaluationContext,
  defaultValue: string,
): Promise<string> {
  return client.getStringValue(flagKey, defaultValue, context);
}

export async function getNumberValue(
  flagKey: string,
  context: EvaluationContext,
  defaultValue: number,
): Promise<number> {
  return client.getNumberValue(flagKey, defaultValue, context);
}

Using Flags in Application Code

// Route handler — flag gates the checkout version
import { isEnabled, buildContext } from '../lib/featureFlags';
import { FLAGS } from '../flags';

app.post('/api/checkout', requireAuth, async (req, res) => {
  const context = buildContext(req.user);
  const useCheckoutV2 = await isEnabled(FLAGS.CHECKOUT_V2, context);
  
  if (useCheckoutV2) {
    return handleCheckoutV2(req, res);
  }
  return handleCheckoutV1(req, res);
});

// React: flag in component
// Client-side flags fetched from an endpoint (don't expose SDK key to browser)
export async function getFlags(userId: string) {
  const context = buildContext(await getUser(userId));
  
  return {
    checkoutV2: await isEnabled(FLAGS.CHECKOUT_V2, context),
    freeShippingThreshold: await getNumberValue(FLAGS.FREE_SHIPPING_THRESHOLD, context, 5000),
  };
}

// Pass flags via server component props or inject at page load
// Never evaluate flags directly in browser — all evaluation happens server-side

LaunchDarkly Targeting Rules

Configure a flag to roll out to:
1. Internal users immediately
2. Beta users after 1 week
3. 10% → 50% → 100% of all users over 2 weeks

LaunchDarkly targeting (via SDK or dashboard):

// Programmatic flag configuration via LaunchDarkly REST API
// (Usually done in LaunchDarkly UI, but can be templated)

const targetingRules = {
  flagKey: 'checkout_v2_enabled',
  rules: [
    // Rule 1: Internal team — always on
    {
      clauses: [{ attribute: 'email', op: 'endsWith', values: ['@mycompany.com'] }],
      variation: 1,  // true
    },
    // Rule 2: Beta users — always on
    {
      clauses: [{ attribute: 'betaUser', op: 'in', values: [true] }],
      variation: 1,
    },
  ],
  // Percentage rollout for everyone else
  fallthrough: {
    rollout: {
      bucketBy: 'key',  // targetingKey — ensures consistent user assignment
      variations: [
        { variation: 0, weight: 90000 },  // 90% → false
        { variation: 1, weight: 10000 },  // 10% → true
      ],
    },
  },
};

Flag-Gated Database Migration

We need to run a risky data migration (backfilling a new column).
Gate it with a flag so we can stop immediately if something breaks.
// src/workers/backfillMigration.ts
import { isEnabled } from '../lib/featureFlags';
import { FLAGS } from '../flags';

const SYSTEM_CONTEXT = {
  targetingKey: 'system',
  environment: process.env.NODE_ENV,
};

export async function runBackfill() {
  // Check flag before each batch — allows mid-migration stop
  const shouldContinue = await isEnabled(FLAGS.NEW_SEARCH, SYSTEM_CONTEXT);
  
  if (!shouldContinue) {
    console.log('Backfill paused by feature flag');
    return { paused: true };
  }
  
  let lastId = 0;
  let processedCount = 0;
  
  while (true) {
    // Re-check flag at each batch (can be toggled mid-run)
    if (!await isEnabled(FLAGS.NEW_SEARCH, SYSTEM_CONTEXT)) {
      console.log(`Backfill paused at ID ${lastId} after ${processedCount} rows`);
      break;
    }
    
    const batch = await db('products')
      .where('id', '>', lastId)
      .whereNull('search_vector')
      .orderBy('id')
      .limit(500)
      .select('id', 'name', 'description');
    
    if (batch.length === 0) {
      console.log(`Backfill complete: ${processedCount} rows processed`);
      break;
    }
    
    await db('products')
      .whereIn('id', batch.map(r => r.id))
      .update({ search_vector: buildSearchVector });  // simplified
    
    lastId = batch[batch.length - 1].id;
    processedCount += batch.length;
    
    await new Promise(r => setTimeout(r, 25));  // Throttle
  }
  
  return { processedCount, lastId };
}

Kill Switch Pattern

// Centralized kill switch check — wrap any expensive or risky operation
export async function withKillSwitch<T>(
  flagKey: string,
  context: EvaluationContext,
  fn: () => Promise<T>,
  fallback: () => Promise<T>,
): Promise<T> {
  const isAlive = await isEnabled(flagKey, context, true);  // Default true = enabled
  
  if (!isAlive) {
    console.warn(`Kill switch activated for: ${flagKey}`);
    return fallback();
  }
  
  return fn();
}

// Usage: wrap the AI recommendation feature
app.get('/api/recommendations', requireAuth, async (req, res) => {
  const result = await withKillSwitch(
    FLAGS.PRODUCT_RECOMMENDATIONS,
    buildContext(req.user),
    () => getAIRecommendations(req.user.id),   // Expensive ML call
    () => getTopSellingProducts(),              // Cheap fallback
  );
  
  res.json(result);
});

Testing with Feature Flags

// Test with specific flag states — don't rely on real LD connection
import { OpenFeature } from '@openfeature/server-sdk';
import { InMemoryProvider } from '@openfeature/in-memory-provider';

beforeEach(async () => {
  // Override the provider for this test
  await OpenFeature.setProviderAndWait(new InMemoryProvider({
    flags: {
      'checkout_v2_enabled': {
        defaultVariant: 'on',
        variants: { on: true, off: false },
      },
    },
  }));
});

test('checkout v2 returns new response shape', async () => {
  const res = await request(app)
    .post('/api/checkout')
    .set('Authorization', `Bearer ${testToken}`)
    .send(checkoutPayload);
  
  expect(res.body).toHaveProperty('paymentIntentClientSecret');  // V2 field
});

For the zero-downtime deployment patterns that feature flags enable (deploy code off, then enable), see the zero-downtime deployments guide. For the CQRS read model migrations that benefit from kill switch patterns, the CQRS guide covers projection backfill strategies. The Claude Skills 360 bundle includes feature flag skill sets covering OpenFeature setup, targeting rules, and migration gating patterns. Start with the free tier to try flag integration templates.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free