The traditional model of software releases conflates two separate events: deployment (putting code into production) and release (making a feature available to users). This coupling creates pressure: deployments become high-stakes, slow, and infrequent because every deployment is also a release.
Feature flags (also called feature toggles) decouple these events. You can deploy code to production that is invisible to all users, then gradually release it to 1%, then 10%, then 100% — or to specific users, or to users in specific countries — without any additional deployment. If a feature misbehaves, you disable the flag and it disappears instantly for all users, without a rollback deployment.
What Feature Flags Enable
WITHOUT FLAGS: WITH FLAGS:
Deploy = Release Deploy ≠ Release
│ │
▼ ▼
High-stakes release Low-stakes deployment
All-or-nothing Gradual rollout
Rollback = new deploy Rollback = flip a switch
No A/B testing Built-in A/B testing
Long release cycles Deploy many times/dayLaunchDarkly Setup
LaunchDarkly is the industry-standard feature flag platform. Install the Next.js SDK:
pnpm add @launchdarkly/node-server-sdk @launchdarkly/js-client-sdk-commonInitialize the server-side client:
// lib/feature-flags/server.ts
import * as ld from '@launchdarkly/node-server-sdk';
let client: ld.LDClient | null = null;
export async function getFeatureFlagClient(): Promise<ld.LDClient> {
if (client) return client;
client = ld.init(process.env.LAUNCHDARKLY_SDK_KEY!);
await client.waitForInitialization({ timeout: 5 });
return client;
}
// Helper: evaluate a flag for the current user
export async function isFeatureEnabled(
flagKey: string,
userId: string,
attributes: Record<string, string> = {}
): Promise<boolean> {
const client = await getFeatureFlagClient();
const context: ld.LDContext = {
kind: 'user',
key: userId,
...attributes,
};
return client.variation(flagKey, context, false);
}
// Helper: get all flags for a user (for client-side hydration)
export async function getAllFlags(userId: string): Promise<ld.LDFlagSet> {
const client = await getFeatureFlagClient();
const context: ld.LDContext = { kind: 'user', key: userId };
return client.allFlagsState(context);
}
Using Flags in Next.js Server Components
// app/dashboard/page.tsx
import { isFeatureEnabled } from '@/lib/feature-flags/server';
import { NewAnalyticsDashboard } from '@/components/NewAnalyticsDashboard';
import { LegacyDashboard } from '@/components/LegacyDashboard';
import { getServerSession } from 'next-auth';
export default async function DashboardPage() {
const session = await getServerSession();
const userId = session?.user.id ?? 'anonymous';
const showNewDashboard = await isFeatureEnabled(
'new-analytics-dashboard',
userId,
{
plan: session?.user.plan ?? 'free',
email: session?.user.email ?? '',
}
);
return (
<main>
{showNewDashboard ? (
<NewAnalyticsDashboard />
) : (
<LegacyDashboard />
)}
</main>
);
}Gradual Rollout Configuration
In the LaunchDarkly dashboard, create a flag new-analytics-dashboard with these targeting rules:
Flag: new-analytics-dashboard
Targeting Rules (evaluated in order):
1. If user.email matches *@sabaoon.dev → serve true (Internal team)
2. If user.plan is "enterprise" → serve true (Beta customers)
3. Percentage rollout: 10% → true, 90% → false (Gradual rollout)
Default (when targeting is off): falseThis releases the feature to:
- Your internal team immediately (for testing).
- Enterprise customers next (as a beta).
- 10% of all other users in a gradual rollout.
Increase the percentage as confidence builds: 10% → 25% → 50% → 100%.
Kill Switch Pattern
The most immediate value of feature flags is the kill switch: if a feature causes errors in production, disable the flag and it's gone for all users within seconds — no deployment required:
// lib/feature-flags/use-with-fallback.ts
export async function withFeatureFlag<T>(
flagKey: string,
userId: string,
enabledFn: () => Promise<T>,
disabledFn: () => Promise<T>
): Promise<T> {
const enabled = await isFeatureEnabled(flagKey, userId);
try {
if (enabled) {
return await enabledFn();
}
return await disabledFn();
} catch (error) {
// If the enabled feature throws, automatically fall back to disabled
console.error(`[FeatureFlag] '${flagKey}' threw an error, falling back:`, error);
return await disabledFn();
}
}
// Usage
const data = await withFeatureFlag(
'new-search-algorithm',
userId,
() => newSearchAlgorithm(query), // New implementation
() => legacySearch(query) // Stable fallback
);
A/B Testing with Feature Flags
Flags support multiple variants (not just true/false) for running A/B tests:
// lib/feature-flags/ab-test.ts
export async function getCheckoutVariant(userId: string): Promise<'control' | 'one-page' | 'wizard'> {
const client = await getFeatureFlagClient();
const context: ld.LDContext = { kind: 'user', key: userId };
// Returns one of the defined string variants
const variant = await client.variation('checkout-flow', context, 'control');
return variant as 'control' | 'one-page' | 'wizard';
}
// Track conversion for the A/B test
export async function trackConversion(userId: string, variant: string) {
const client = await getFeatureFlagClient();
// LaunchDarkly's experimentation tracks this automatically
client.track('checkout-completed', { kind: 'user', key: userId }, { variant });
}Secrets Management: Keeping SDK Keys Safe
# .env.local
LAUNCHDARKLY_SDK_KEY=sdk-your-server-side-key
NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID=your-client-side-idNever expose your server-side SDK key to the client. It provides full read/write access to your LaunchDarkly project. The NEXT_PUBLIC_ client ID is safe to expose — it's read-only.
Self-Hosted Alternative: Unleash
If you need to keep flag data on your own infrastructure for compliance reasons, Unleash is an open-source alternative:
# Run Unleash locally with Docker
docker run -d --name unleash \
-p 4242:4242 \
-e DATABASE_URL=postgres://user:pass@localhost:5432/unleash \
unleashorg/unleash-serverimport { initialize } from 'unleash-client';
const unleash = initialize({
url: 'http://localhost:4242/api/',
appName: 'my-app',
customHeaders: { Authorization: process.env.UNLEASH_API_TOKEN! },
});
const isEnabled = unleash.isEnabled('new-checkout-flow');Conclusion
Feature flags are the engineering practice that enables high-deployment-frequency teams to move fast without breaking things. By separating deployment from release, you remove the risk from each deployment, gain the ability to do gradual rollouts and A/B tests, and get kill switches that can disable a misbehaving feature in seconds. LaunchDarkly makes this achievable for any Next.js application with a single package and a few lines of configuration. The cultural shift — from thinking in "releases" to thinking in "deployments" — is what ultimately enables continuous delivery.