Skip to main content
Technology & EngineeringFeature Flags Services321 lines

LaunchDarkly

"LaunchDarkly: feature flags, targeting rules, segments, experiments, metrics, Node/React SDK, bootstrap, streaming"

Quick Summary18 lines
LaunchDarkly is a feature management platform that decouples deployment from release. Every feature ships behind a flag, giving teams precise control over who sees what, when. The platform emphasizes real-time flag evaluation via streaming connections, rich targeting with user segments, and built-in experimentation to measure the impact of features before full rollout. Flags are not just booleans — they are multivariate controls that can return strings, numbers, JSON objects, enabling dynamic configuration at scale.

## Key Points

1. **Use multi-contexts** to model real-world relationships (user, organization, device). This enables targeting at every level without duplicating attributes.
2. **Bootstrap flags on the server** for SSR applications. This eliminates the flash of default content while the client SDK initializes its streaming connection.
3. **Keep flag lifecycles short.** Create a flag, roll it out, confirm it works, then remove the flag and dead code path. Stale flags accumulate tech debt.
4. **Use variation detail** (`variationDetail`) during debugging and experiment analysis to understand _why_ a particular value was served.
5. **Track metrics close to the user action.** Call `track()` immediately when the event occurs rather than batching in application code — the SDK handles batching internally.
6. **Namespace flag keys** by domain: `checkout-express-enabled`, `search-v2-algorithm`, `billing-retry-limit`. This scales across teams.
7. **Set meaningful defaults.** The fallback value in `variation()` is used when the SDK cannot reach LaunchDarkly. Defaults should always be the safe, existing behavior.
1. **Evaluating flags in tight loops.** Flag evaluation is fast but not free. Cache the result in a local variable when evaluating inside loops or repeated renders.
2. **Using flags as a database.** Flags are for feature control, not persistent user preferences. Store user settings in your own data layer.
3. **Anonymous contexts without stable keys.** If you generate a new random key on every request, percentage rollouts become random rather than sticky. Use cookies or device IDs for consistency.
4. **Ignoring flag prerequisites.** If flag B depends on flag A being on, model this as a prerequisite in LaunchDarkly rather than nesting conditionals in code — the platform enforces the dependency.
5. **Never cleaning up flags.** Every flag that ships successfully should have a removal ticket. Use LaunchDarkly's code references and stale flag detection to find flags safe to remove.
skilldb get feature-flags-services-skills/LaunchDarklyFull skill: 321 lines
Paste into your CLAUDE.md or agent config

LaunchDarkly

Core Philosophy

LaunchDarkly is a feature management platform that decouples deployment from release. Every feature ships behind a flag, giving teams precise control over who sees what, when. The platform emphasizes real-time flag evaluation via streaming connections, rich targeting with user segments, and built-in experimentation to measure the impact of features before full rollout. Flags are not just booleans — they are multivariate controls that can return strings, numbers, JSON objects, enabling dynamic configuration at scale.

Setup

Node.js Server SDK

import * as LaunchDarkly from "@launchdarkly/node-server-sdk";

const client = LaunchDarkly.init(process.env.LAUNCHDARKLY_SDK_KEY!);

await client.waitForInitialization({ timeout: 5 });

// Evaluate a boolean flag
const context: LaunchDarkly.LDContext = {
  kind: "user",
  key: "user-123",
  email: "alice@example.com",
  custom: {
    plan: "enterprise",
    region: "us-east-1",
  },
};

const showNewDashboard = await client.variation(
  "new-dashboard",
  context,
  false // default value
);

if (showNewDashboard) {
  renderNewDashboard();
} else {
  renderLegacyDashboard();
}

React SDK with Bootstrap

// app/providers.tsx
import { LDProvider } from "launchdarkly-react-client-sdk";

interface Props {
  children: React.ReactNode;
  bootstrapFlags: Record<string, unknown>;
  userKey: string;
}

export function FeatureFlagProvider({
  children,
  bootstrapFlags,
  userKey,
}: Props) {
  return (
    <LDProvider
      clientSideID={process.env.NEXT_PUBLIC_LD_CLIENT_ID!}
      context={{
        kind: "user",
        key: userKey,
        anonymous: false,
      }}
      options={{
        bootstrap: bootstrapFlags,
        streaming: true,
        sendEventsOnlyForVariation: true,
      }}
    >
      {children}
    </LDProvider>
  );
}

Server-Side Bootstrap for SSR

// app/layout.tsx — Next.js App Router
import * as LaunchDarkly from "@launchdarkly/node-server-sdk";

const ldServer = LaunchDarkly.init(process.env.LD_SDK_KEY!);

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  await ldServer.waitForInitialization({ timeout: 5 });

  const context: LaunchDarkly.LDContext = {
    kind: "user",
    key: "anonymous",
    anonymous: true,
  };

  const allFlags = await ldServer.allFlagsState(context);
  const bootstrapFlags = allFlags.toJSON();

  return (
    <html>
      <body>
        <FeatureFlagProvider
          bootstrapFlags={bootstrapFlags}
          userKey="anonymous"
        >
          {children}
        </FeatureFlagProvider>
      </body>
    </html>
  );
}

Key Techniques

Multi-Context Targeting

// Target by user AND organization simultaneously
const multiContext: LaunchDarkly.LDContext = {
  kind: "multi",
  user: {
    key: "user-456",
    email: "bob@acme.com",
    custom: { role: "admin" },
  },
  organization: {
    key: "org-acme",
    name: "Acme Corp",
    custom: { plan: "enterprise", employeeCount: 500 },
  },
  device: {
    key: "device-abc",
    custom: { os: "ios", appVersion: "3.2.1" },
  },
};

const limit = await client.variation("api-rate-limit", multiContext, 1000);

Multivariate Flags and JSON Variations

// Flag returning a JSON config object
interface CheckoutConfig {
  maxRetries: number;
  timeoutMs: number;
  provider: "stripe" | "adyen" | "braintree";
  showExpressCheckout: boolean;
}

const checkoutConfig = await client.jsonVariation<CheckoutConfig>(
  "checkout-config",
  context,
  {
    maxRetries: 3,
    timeoutMs: 5000,
    provider: "stripe",
    showExpressCheckout: false,
  }
);

const paymentClient = createPaymentClient({
  provider: checkoutConfig.provider,
  timeout: checkoutConfig.timeoutMs,
  retries: checkoutConfig.maxRetries,
});

Listening for Real-Time Flag Changes

// React hook for live updates
import { useFlags, useLDClient } from "launchdarkly-react-client-sdk";
import { useEffect } from "react";

function FeatureAwareComponent() {
  const flags = useFlags();
  const ldClient = useLDClient();

  useEffect(() => {
    const handler = (newValue: boolean, oldValue: boolean) => {
      console.log(`Flag changed: ${oldValue} -> ${newValue}`);
      // Optionally trigger analytics or UI transitions
    };

    ldClient?.on("change:new-checkout-flow", handler);
    return () => {
      ldClient?.off("change:new-checkout-flow", handler);
    };
  }, [ldClient]);

  if (flags["new-checkout-flow"]) {
    return <NewCheckout />;
  }
  return <LegacyCheckout />;
}

Experimentation and Metrics

// Track custom metric events for experiments
const ldClient = useLDClient();

function handlePurchaseComplete(orderId: string, revenue: number) {
  // Numeric metric for revenue tracking
  ldClient?.track("purchase-completed", undefined, revenue);

  // Conversion metric
  ldClient?.track("checkout-conversion");

  // Metric with additional data
  ldClient?.track("order-placed", {
    orderId,
    itemCount: cart.items.length,
    paymentMethod: selectedMethod,
  });
}

// Server-side experiment tracking
async function processOrder(userId: string, order: Order) {
  const context: LaunchDarkly.LDContext = {
    kind: "user",
    key: userId,
  };

  // Evaluate which variation the user is in
  const detail = await client.variationDetail(
    "pricing-experiment",
    context,
    "control"
  );

  // detail.variationIndex tells you which bucket
  // detail.reason explains why (e.g., "RULE_MATCH", "FALLTHROUGH")

  client.track("order-value", context, null, order.total);
  client.track("conversion", context);
}

Segments and Progressive Rollouts

// Middleware for percentage-based rollout with sticky assignment
import { NextRequest, NextResponse } from "next/server";

export async function middleware(req: NextRequest) {
  await ldServer.waitForInitialization({ timeout: 5 });

  const userId = req.cookies.get("uid")?.value ?? "anonymous";

  const context: LaunchDarkly.LDContext = {
    kind: "user",
    key: userId,
    custom: {
      country: req.geo?.country ?? "unknown",
      pathname: req.nextUrl.pathname,
    },
  };

  const detail = await ldServer.variationDetail(
    "new-landing-page",
    context,
    false
  );

  if (detail.value && req.nextUrl.pathname === "/") {
    const url = req.nextUrl.clone();
    url.pathname = "/new-landing";
    return NextResponse.rewrite(url);
  }

  return NextResponse.next();
}

Best Practices

  1. Use multi-contexts to model real-world relationships (user, organization, device). This enables targeting at every level without duplicating attributes.

  2. Bootstrap flags on the server for SSR applications. This eliminates the flash of default content while the client SDK initializes its streaming connection.

  3. Keep flag lifecycles short. Create a flag, roll it out, confirm it works, then remove the flag and dead code path. Stale flags accumulate tech debt.

  4. Use variation detail (variationDetail) during debugging and experiment analysis to understand why a particular value was served.

  5. Track metrics close to the user action. Call track() immediately when the event occurs rather than batching in application code — the SDK handles batching internally.

  6. Namespace flag keys by domain: checkout-express-enabled, search-v2-algorithm, billing-retry-limit. This scales across teams.

  7. Set meaningful defaults. The fallback value in variation() is used when the SDK cannot reach LaunchDarkly. Defaults should always be the safe, existing behavior.

  8. Use segments for reusable audiences. Define "beta testers" or "enterprise accounts" as segments in the dashboard, then reference them across multiple flags instead of duplicating targeting rules.

Anti-Patterns

  1. Evaluating flags in tight loops. Flag evaluation is fast but not free. Cache the result in a local variable when evaluating inside loops or repeated renders.

  2. Using flags as a database. Flags are for feature control, not persistent user preferences. Store user settings in your own data layer.

  3. Anonymous contexts without stable keys. If you generate a new random key on every request, percentage rollouts become random rather than sticky. Use cookies or device IDs for consistency.

  4. Ignoring flag prerequisites. If flag B depends on flag A being on, model this as a prerequisite in LaunchDarkly rather than nesting conditionals in code — the platform enforces the dependency.

  5. Never cleaning up flags. Every flag that ships successfully should have a removal ticket. Use LaunchDarkly's code references and stale flag detection to find flags safe to remove.

  6. Putting SDK keys in client bundles. The client-side ID is meant for browsers, but the server SDK key must stay on the server. Leaking it exposes your full flag configuration and targeting rules.

  7. Wrapping the SDK in unnecessary abstractions. A thin wrapper for testability is fine, but deep abstraction layers hide the SDK's streaming and caching behavior, leading to stale evaluations or missed updates.

Install this skill directly: skilldb add feature-flags-services-skills

Get CLI access →