Feature flags decouple deployment from release — you can ship code to production before it’s visible to users, gradually roll out to 1% → 10% → 100%, and instantly kill a feature if something goes wrong. Claude Code understands flag patterns well enough to implement them correctly: server-side evaluation to prevent flicker, consistent bucketing for UX coherence, and clean code that’s easy to remove when flags are retired.
This guide covers feature flags with Claude Code: implementation patterns, gradual rollouts, testing, and flag lifecycle management.
OpenFeature (Provider-Agnostic)
Set up feature flags in our Node.js API.
We want to be able to switch providers (LaunchDarkly → Flagsmith)
without rewriting application code.
// src/feature-flags/index.ts
import { OpenFeature, Client } from '@openfeature/server-sdk';
import { LaunchDarklyProvider } from '@openfeature/launchdarkly-provider';
// Initialize once at app startup
export async function initFeatureFlags(): Promise<void> {
const provider = new LaunchDarklyProvider(process.env.LAUNCHDARKLY_SDK_KEY!);
await OpenFeature.setProviderAndWait(provider);
}
// Get a client with user context
export function getFlagClient(user: { id: string; email: string; plan: string }): Client {
return OpenFeature.getClient();
}
// Typed flag evaluation helpers
export const flags = {
async newDashboard(client: Client, userId: string): Promise<boolean> {
return client.getBooleanValue(
'new-dashboard',
false, // Default: off
{ targetingKey: userId }
);
},
async checkoutV2(client: Client, userId: string): Promise<boolean> {
return client.getBooleanValue('checkout-v2', false, { targetingKey: userId });
},
async aiSuggestions(client: Client, userId: string): Promise<boolean> {
return client.getBooleanValue('ai-suggestions', false, { targetingKey: userId });
},
async maxUploadSizeMb(client: Client, userId: string): Promise<number> {
return client.getNumberValue('max-upload-size-mb', 10, { targetingKey: userId });
},
} as const;
// Using flags in a route
app.get('/api/dashboard', async (req, res) => {
const client = getFlagClient(req.user);
const useNewDashboard = await flags.newDashboard(client, req.user.id);
if (useNewDashboard) {
const data = await getDashboardV2Data(req.user.id);
return res.json({ version: 'v2', ...data });
}
const data = await getDashboardData(req.user.id);
return res.json({ version: 'v1', ...data });
});
Client-Side Flags (React)
Add feature flags to the React frontend.
Prevent flicker — don't show the old UI while the flag loads.
// src/hooks/useFeatureFlag.ts
import { useEffect, useState } from 'react';
import { OpenFeature } from '@openfeature/web-sdk';
import { useAuth } from './useAuth';
// Server-renders with defaults to prevent flash of old content
const flagDefaults: Record<string, boolean | string | number> = {
'new-dashboard': false,
'dark-mode-enabled': false,
'max-file-upload-mb': 10,
};
export function useBooleanFlag(flagKey: string): { value: boolean; loading: boolean } {
const { user } = useAuth();
const [value, setValue] = useState<boolean>(
() => flagDefaults[flagKey] as boolean ?? false
);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!user) return;
const client = OpenFeature.getClient();
// Evaluate immediately if provider is ready
setValue(client.getBooleanValue(flagKey, flagDefaults[flagKey] as boolean ?? false));
setLoading(false);
// Re-evaluate if flag changes (real-time streaming)
const handler = () => {
setValue(client.getBooleanValue(flagKey, flagDefaults[flagKey] as boolean ?? false));
};
client.addHandler('PROVIDER_CONFIGURATION_CHANGED', handler);
return () => client.removeHandler('PROVIDER_CONFIGURATION_CHANGED', handler);
}, [flagKey, user?.id]);
return { value, loading };
}
// Usage in components — shows default during loading (no flicker)
export function Dashboard() {
const { value: showNewDashboard, loading } = useBooleanFlag('new-dashboard');
if (loading) {
// Render a skeleton that works for both versions — no layout shift
return <DashboardSkeleton />;
}
return showNewDashboard ? <DashboardV2 /> : <DashboardV1 />;
}
Gradual Rollout Implementation
We want to roll out the new payment flow to 5% of users,
then 20%, then 50%, then 100%. The same user should always
get the same experience (consistent bucketing).
For simple rollouts without an external service:
// src/lib/rollout.ts
import { createHash } from 'crypto';
// Deterministic bucketing: same userId always maps to same number 0-99
function getBucket(userId: string, flagKey: string): number {
const hash = createHash('sha256')
.update(`${flagKey}:${userId}`)
.digest('hex');
// Take first 8 hex chars → convert to integer → mod 100
return parseInt(hash.slice(0, 8), 16) % 100;
}
export function isInRollout(
userId: string,
flagKey: string,
rolloutPercentage: number,
): boolean {
if (rolloutPercentage >= 100) return true;
if (rolloutPercentage <= 0) return false;
return getBucket(userId, flagKey) < rolloutPercentage;
}
// Database-backed flag config
interface FlagConfig {
key: string;
enabled: boolean;
rolloutPercentage: number;
allowlist: string[]; // Specific user IDs that always get the flag
denylist: string[]; // Specific user IDs that never get it
}
export async function evaluateFlag(
flagKey: string,
userId: string,
): Promise<boolean> {
const config = await getFlagConfig(flagKey); // From Redis/DB with short TTL cache
if (!config?.enabled) return false;
if (config.denylist.includes(userId)) return false;
if (config.allowlist.includes(userId)) return true;
return isInRollout(userId, flagKey, config.rolloutPercentage);
}
Testing Code Behind Flags
Write tests for components and APIs that are behind feature flags.
Need to test both the old and new behavior.
// tests/dashboard.test.ts
import { vi } from 'vitest';
import * as featureFlags from '../src/feature-flags';
describe('Dashboard API', () => {
describe('with new-dashboard flag OFF', () => {
beforeEach(() => {
vi.spyOn(featureFlags.flags, 'newDashboard').mockResolvedValue(false);
});
it('returns v1 dashboard data', async () => {
const response = await supertest(app)
.get('/api/dashboard')
.set('Authorization', `Bearer ${userToken}`);
expect(response.status).toBe(200);
expect(response.body.version).toBe('v1');
});
});
describe('with new-dashboard flag ON', () => {
beforeEach(() => {
vi.spyOn(featureFlags.flags, 'newDashboard').mockResolvedValue(true);
});
it('returns v2 dashboard data', async () => {
const response = await supertest(app)
.get('/api/dashboard')
.set('Authorization', `Bearer ${userToken}`);
expect(response.status).toBe(200);
expect(response.body.version).toBe('v2');
});
});
});
// React component test
import { render, screen } from '@testing-library/react';
import * as flagHooks from '../hooks/useFeatureFlag';
test('renders V2 dashboard when flag is enabled', () => {
vi.spyOn(flagHooks, 'useBooleanFlag').mockReturnValue({
value: true,
loading: false,
});
render(<Dashboard />);
expect(screen.getByTestId('dashboard-v2')).toBeInTheDocument();
expect(screen.queryByTestId('dashboard-v1')).not.toBeInTheDocument();
});
Flag Lifecycle and Cleanup
We have 30 old flags that were never cleaned up.
Help me audit and remove them.
Claude Code generates a cleanup process:
// scripts/audit-flags.ts
import { getAllFlags } from './src/feature-flags';
// Find flags where rollout is 100% (can remove the flag, just ship the code)
// Find flags where rollout is 0% (dead code — remove it)
const flags = await getAllFlags();
const stale = flags.filter(f =>
f.rolloutPercentage === 100 || f.rolloutPercentage === 0 || f.archivedAt
);
console.log('Stale flags to clean up:');
stale.forEach(f => {
const reason = f.rolloutPercentage === 100 ? 'fully rolled out'
: f.rolloutPercentage === 0 ? 'never rolled out (dead code)'
: 'archived';
console.log(` ${f.key}: ${reason} (created ${f.createdAt})`);
});
For A/B testing that goes beyond binary feature flags to measure statistical significance, the performance guide covers experiment design. For deploying behind feature flags safely with zero-downtime database migrations, see the database migrations guide. The Claude Skills 360 bundle includes feature flag skill sets for gradual rollouts and A/B testing patterns. Start with the free tier to add feature flags to your deployment pipeline.