Skip to main content

Feature Flags with LaunchDarkly: Decoupling Deployment from Release

June 2, 2026

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/day

LaunchDarkly Setup

LaunchDarkly is the industry-standard feature flag platform. Install the Next.js SDK:

pnpm add @launchdarkly/node-server-sdk @launchdarkly/js-client-sdk-common

Initialize 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): false

This 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-id

Never 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-server
import { 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.

Recommended Posts