Skip to main content
Technology & EngineeringFeature Flags Services461 lines

GrowthBook

"GrowthBook: open-source feature flags, A/B testing, Bayesian statistics, SDK, targeting, webhooks, self-hosted"

Quick Summary15 lines
GrowthBook is an open-source feature flagging and experimentation platform built around rigorous statistical analysis. While many feature flag tools add experimentation as an afterthought, GrowthBook treats Bayesian statistics as a first-class concern — providing credible intervals, chance to beat baseline, and expected loss calculations out of the box. The SDK evaluates flags locally using a downloaded feature definitions document, meaning zero latency per flag check. The platform connects directly to your existing data warehouse (BigQuery, Snowflake, Postgres, etc.) to compute experiment results rather than requiring a proprietary event pipeline.

## Key Points

1. **Create one GrowthBook instance per request on the server.** Instances hold user attributes and track exposures. Sharing instances across requests leaks user context.
2. **Always call `destroy()` when done.** This cleans up streaming connections and internal state. In server contexts, destroy at the end of the request lifecycle.
5. **Cache feature definitions aggressively.** The feature definitions endpoint is a static JSON document. Cache it in-memory with a short TTL and use webhooks for instant invalidation.
6. **Use streaming mode on the client.** The React SDK supports SSE streaming so flag changes propagate in real time without page reloads.
7. **Set `hashAttribute` to a stable identifier.** GrowthBook uses deterministic hashing for variation assignment. Use a user ID, not a session ID, to ensure users see consistent experiences.
8. **Review Bayesian results with proper priors.** GrowthBook defaults to uninformative priors. If you have historical data about metric distributions, configure priors for faster convergence.
2. **Not logging exposure events.** Without exposure data in your warehouse, GrowthBook cannot compute experiment results. The `trackingCallback` is not optional for experimentation.
3. **Evaluating flags before `init()` completes.** If you call `gb.isOn()` before features load, you get default values. Always await `init()` or handle the loading state explicitly.
6. **Forgetting to set attributes before evaluation.** GrowthBook evaluates targeting conditions against the attributes you provide. Missing attributes cause rules to be skipped silently.
skilldb get feature-flags-services-skills/GrowthBookFull skill: 461 lines
Paste into your CLAUDE.md or agent config

GrowthBook

Core Philosophy

GrowthBook is an open-source feature flagging and experimentation platform built around rigorous statistical analysis. While many feature flag tools add experimentation as an afterthought, GrowthBook treats Bayesian statistics as a first-class concern — providing credible intervals, chance to beat baseline, and expected loss calculations out of the box. The SDK evaluates flags locally using a downloaded feature definitions document, meaning zero latency per flag check. The platform connects directly to your existing data warehouse (BigQuery, Snowflake, Postgres, etc.) to compute experiment results rather than requiring a proprietary event pipeline.

Setup

Node.js / TypeScript SDK

import { GrowthBook, setPolyfills } from "@growthbook/growthbook";

// Server-side: create a GrowthBook instance per request
function createGrowthBook(
  userId: string,
  attributes: Record<string, unknown>
): GrowthBook {
  const gb = new GrowthBook({
    apiHost: process.env.GROWTHBOOK_API_HOST ?? "https://cdn.growthbook.io",
    clientKey: process.env.GROWTHBOOK_CLIENT_KEY!,
    decryptionKey: process.env.GROWTHBOOK_DECRYPT_KEY,
    enabled: true,
    attributes: {
      id: userId,
      ...attributes,
    },
    trackingCallback: (experiment, result) => {
      // Send exposure event to your analytics/data warehouse
      trackExperimentExposure({
        experimentId: experiment.key,
        variationId: result.key,
        userId,
        timestamp: new Date().toISOString(),
      });
    },
  });

  return gb;
}

// Initialize and use
async function handleRequest(userId: string) {
  const gb = createGrowthBook(userId, {
    plan: "pro",
    country: "US",
    browser: "chrome",
    employee: false,
  });

  await gb.init({ timeout: 3000 });

  const showNewFeature = gb.isOn("new-checkout-flow");
  const buttonColor = gb.getFeatureValue("cta-button-color", "blue");

  // Always destroy when done to clean up
  gb.destroy();

  return { showNewFeature, buttonColor };
}

Next.js App Router Integration

// lib/growthbook-server.ts
import { GrowthBook } from "@growthbook/growthbook";
import { cache } from "react";

let featuresCache: Record<string, unknown> | null = null;
let lastFetch = 0;
const CACHE_TTL_MS = 30_000;

async function fetchFeatures(): Promise<Record<string, unknown>> {
  const now = Date.now();
  if (featuresCache && now - lastFetch < CACHE_TTL_MS) {
    return featuresCache;
  }

  const res = await fetch(
    `${process.env.GROWTHBOOK_API_HOST}/api/features/${process.env.GROWTHBOOK_CLIENT_KEY}`,
    { next: { revalidate: 30 } }
  );
  const data = await res.json();
  featuresCache = data.features;
  lastFetch = now;
  return featuresCache!;
}

// React cache ensures one GrowthBook instance per request
export const getServerGrowthBook = cache(
  async (userId: string, attributes: Record<string, unknown>) => {
    const features = await fetchFeatures();

    const gb = new GrowthBook({
      features,
      attributes: { id: userId, ...attributes },
      trackingCallback: (experiment, result) => {
        // Queue exposure for batch insert into warehouse
        queueExposureEvent(userId, experiment.key, result.key);
      },
    });

    return gb;
  }
);
// app/page.tsx — Server Component flag evaluation
import { getServerGrowthBook } from "@/lib/growthbook-server";
import { cookies } from "next/headers";

export default async function HomePage() {
  const cookieStore = await cookies();
  const userId = cookieStore.get("uid")?.value ?? "anonymous";

  const gb = await getServerGrowthBook(userId, {
    url: "/",
    loggedIn: userId !== "anonymous",
  });

  const showHero = gb.isOn("new-hero-section");
  const heroConfig = gb.getFeatureValue("hero-config", {
    title: "Welcome",
    subtitle: "Get started today",
    ctaText: "Sign Up",
  });

  gb.destroy();

  return (
    <main>
      {showHero ? (
        <NewHero title={heroConfig.title} cta={heroConfig.ctaText} />
      ) : (
        <LegacyHero />
      )}
    </main>
  );
}

React Client SDK

// components/growthbook-provider.tsx
"use client";

import { GrowthBook, GrowthBookProvider } from "@growthbook/growthbook-react";
import { useEffect, useMemo } from "react";

interface Props {
  children: React.ReactNode;
  userId: string;
  attributes?: Record<string, unknown>;
}

export function GBProvider({ children, userId, attributes }: Props) {
  const gb = useMemo(
    () =>
      new GrowthBook({
        apiHost: process.env.NEXT_PUBLIC_GB_API_HOST!,
        clientKey: process.env.NEXT_PUBLIC_GB_CLIENT_KEY!,
        attributes: { id: userId, ...attributes },
        enableDevMode: process.env.NODE_ENV === "development",
        trackingCallback: (experiment, result) => {
          // Client-side analytics tracking
          window.analytics?.track("Experiment Viewed", {
            experimentId: experiment.key,
            variationId: result.key,
          });
        },
      }),
    [userId, attributes]
  );

  useEffect(() => {
    gb.init({ streaming: true });
    return () => gb.destroy();
  }, [gb]);

  return <GrowthBookProvider growthbook={gb}>{children}</GrowthBookProvider>;
}

Key Techniques

Feature Flags with Typed Values

import { useFeatureIsOn, useFeatureValue } from "@growthbook/growthbook-react";

interface SearchConfig {
  algorithm: "keyword" | "semantic" | "hybrid";
  maxResults: number;
  boostFactor: number;
  enableTypoTolerance: boolean;
}

function SearchPage() {
  const isNewSearchEnabled = useFeatureIsOn("new-search-engine");
  const searchConfig = useFeatureValue<SearchConfig>("search-config", {
    algorithm: "keyword",
    maxResults: 20,
    boostFactor: 1.0,
    enableTypoTolerance: false,
  });

  return (
    <SearchInterface
      algorithm={searchConfig.algorithm}
      maxResults={searchConfig.maxResults}
      typoTolerance={searchConfig.enableTypoTolerance}
    />
  );
}

A/B Testing with Experiment Tracking

// GrowthBook assigns variations deterministically based on user ID + experiment key
// The trackingCallback fires once per experiment per user

function createExperimentTracker() {
  const seen = new Set<string>();

  return (experiment: { key: string }, result: { key: string; variationId: number }) => {
    const exposureKey = `${experiment.key}::${result.key}`;
    if (seen.has(exposureKey)) return;
    seen.add(exposureKey);

    // Insert into your data warehouse
    insertExposure({
      experiment_id: experiment.key,
      variation_id: result.variationId,
      user_id: getCurrentUserId(),
      timestamp: new Date().toISOString(),
      // Include attributes for segmented analysis
      attributes: getCurrentAttributes(),
    });
  };
}

Targeting with Complex Conditions

// GrowthBook supports rich targeting conditions in its feature definitions
// These are configured in the dashboard and evaluated locally by the SDK

// Example feature definition (what the SDK receives from the API):
const featureDefinition = {
  defaultValue: false,
  rules: [
    {
      // Force on for internal employees
      condition: { employee: true },
      force: true,
    },
    {
      // 50/50 experiment for US pro users
      condition: {
        country: "US",
        plan: { $in: ["pro", "enterprise"] },
      },
      variations: [false, true],
      weights: [0.5, 0.5],
      hashAttribute: "id",
      key: "checkout-experiment-us",
    },
    {
      // 10% rollout for everyone else
      force: true,
      coverage: 0.1,
      hashAttribute: "id",
    },
  ],
};

// In your code, just check the flag — the SDK handles all the rule evaluation
const gb = createGrowthBook(userId, { country: "US", plan: "pro", employee: false });
await gb.init({ timeout: 3000 });

const enabled = gb.isOn("new-checkout-flow");
// The SDK evaluates rules top-to-bottom, returning the first match

Webhook-Driven Cache Invalidation

// When features change in GrowthBook, a webhook can notify your service
// to refresh its local feature cache immediately

import { createServer, IncomingMessage, ServerResponse } from "http";
import crypto from "crypto";

function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// Express/Next.js route handler for GrowthBook webhooks
export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get("x-growthbook-signature") ?? "";

  if (!verifyWebhookSignature(body, signature, process.env.GB_WEBHOOK_SECRET!)) {
    return new Response("Invalid signature", { status: 401 });
  }

  const event = JSON.parse(body);

  if (event.type === "feature.updated" || event.type === "feature.created") {
    // Invalidate local cache so next request fetches fresh features
    invalidateFeatureCache();
    console.log(`Feature cache invalidated due to: ${event.type}`);
  }

  return new Response("OK", { status: 200 });
}

Connecting to Your Data Warehouse

// GrowthBook queries your warehouse directly for experiment analysis
// You define SQL metric definitions in the dashboard

// Example: defining a metric via the GrowthBook API
async function createMetric(metric: {
  name: string;
  type: "binomial" | "count" | "revenue" | "duration";
  sql: string;
}) {
  const res = await fetch(`${process.env.GROWTHBOOK_API_HOST}/api/v1/metrics`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.GROWTHBOOK_API_SECRET}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      datasourceId: process.env.GROWTHBOOK_DATASOURCE_ID,
      name: metric.name,
      type: metric.type,
      sql: metric.sql,
      // Bayesian prior configuration
      priorSettings: {
        override: false,
        proper: true,
        mean: 0,
        stddev: 0.3,
      },
    }),
  });

  return res.json();
}

// Revenue metric: GrowthBook computes Bayesian stats from your raw data
await createMetric({
  name: "Revenue per User",
  type: "revenue",
  sql: `
    SELECT
      user_id,
      timestamp,
      revenue as value
    FROM orders
    WHERE status = 'completed'
  `,
});

Middleware-Based Feature Routing

// middleware.ts — flag evaluation at the edge
import { GrowthBook } from "@growthbook/growthbook";
import { NextRequest, NextResponse } from "next/server";

export async function middleware(req: NextRequest) {
  const userId = req.cookies.get("uid")?.value ?? "anonymous";

  const gb = new GrowthBook({
    apiHost: process.env.GROWTHBOOK_API_HOST!,
    clientKey: process.env.GROWTHBOOK_CLIENT_KEY!,
    attributes: {
      id: userId,
      url: req.nextUrl.pathname,
      country: req.geo?.country ?? "unknown",
    },
  });

  await gb.init({ timeout: 2000 });

  // A/B test different landing pages
  const landingVariant = gb.getFeatureValue("landing-page-variant", "control");

  gb.destroy();

  if (landingVariant !== "control" && req.nextUrl.pathname === "/") {
    const url = req.nextUrl.clone();
    url.pathname = `/landing/${landingVariant}`;
    return NextResponse.rewrite(url);
  }

  return NextResponse.next();
}

Best Practices

  1. Create one GrowthBook instance per request on the server. Instances hold user attributes and track exposures. Sharing instances across requests leaks user context.

  2. Always call destroy() when done. This cleans up streaming connections and internal state. In server contexts, destroy at the end of the request lifecycle.

  3. Use your existing data warehouse for metrics. GrowthBook's power is that it queries your warehouse directly. You do not need to pipe events through GrowthBook — just ensure exposure events land in the same warehouse.

  4. Configure the tracking callback to deduplicate. The SDK may call trackingCallback multiple times for the same experiment/variation pair within a request. Use a Set to deduplicate before inserting.

  5. Cache feature definitions aggressively. The feature definitions endpoint is a static JSON document. Cache it in-memory with a short TTL and use webhooks for instant invalidation.

  6. Use streaming mode on the client. The React SDK supports SSE streaming so flag changes propagate in real time without page reloads.

  7. Set hashAttribute to a stable identifier. GrowthBook uses deterministic hashing for variation assignment. Use a user ID, not a session ID, to ensure users see consistent experiences.

  8. Review Bayesian results with proper priors. GrowthBook defaults to uninformative priors. If you have historical data about metric distributions, configure priors for faster convergence.

Anti-Patterns

  1. Reusing a single GrowthBook instance across users. Each instance holds attributes for one user. Reusing it means the second user inherits the first user's attributes and experiment assignments.

  2. Not logging exposure events. Without exposure data in your warehouse, GrowthBook cannot compute experiment results. The trackingCallback is not optional for experimentation.

  3. Evaluating flags before init() completes. If you call gb.isOn() before features load, you get default values. Always await init() or handle the loading state explicitly.

  4. Overriding variation assignment in code. If you add logic around the flag check (e.g., "if user is in group X, always show variant B"), you break the randomization that makes experiment statistics valid.

  5. Running experiments without enough traffic. Bayesian analysis needs data. If your experiment has fewer than a few hundred users per variation, the credible intervals will be too wide to draw conclusions.

  6. Forgetting to set attributes before evaluation. GrowthBook evaluates targeting conditions against the attributes you provide. Missing attributes cause rules to be skipped silently.

  7. Ignoring the "chance to beat baseline" metric. GrowthBook shows this prominently. A 70% chance to beat baseline is not statistically meaningful. Wait for at least 95% before making ship decisions.

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

Get CLI access →