GrowthBook
"GrowthBook: open-source feature flags, A/B testing, Bayesian statistics, SDK, targeting, webhooks, self-hosted"
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 linesGrowthBook
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
-
Create one GrowthBook instance per request on the server. Instances hold user attributes and track exposures. Sharing instances across requests leaks user context.
-
Always call
destroy()when done. This cleans up streaming connections and internal state. In server contexts, destroy at the end of the request lifecycle. -
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.
-
Configure the tracking callback to deduplicate. The SDK may call
trackingCallbackmultiple times for the same experiment/variation pair within a request. Use a Set to deduplicate before inserting. -
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.
-
Use streaming mode on the client. The React SDK supports SSE streaming so flag changes propagate in real time without page reloads.
-
Set
hashAttributeto a stable identifier. GrowthBook uses deterministic hashing for variation assignment. Use a user ID, not a session ID, to ensure users see consistent experiences. -
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
-
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.
-
Not logging exposure events. Without exposure data in your warehouse, GrowthBook cannot compute experiment results. The
trackingCallbackis not optional for experimentation. -
Evaluating flags before
init()completes. If you callgb.isOn()before features load, you get default values. Always awaitinit()or handle the loading state explicitly. -
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.
-
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.
-
Forgetting to set attributes before evaluation. GrowthBook evaluates targeting conditions against the attributes you provide. Missing attributes cause rules to be skipped silently.
-
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
Related Skills
ConfigCat
"ConfigCat: feature flags and remote config with percentage rollouts, targeting rules, config-as-code, and cross-platform SDKs"
Flagsmith
"Flagsmith: open-source feature flags, remote config, segments, environments, audit logs, self-hosted, REST API"
Flipt
"Flipt: open-source, self-hosted feature flag platform with GitOps support, boolean and multivariate flags, and GRPC/REST APIs"
LaunchDarkly
"LaunchDarkly: feature flags, targeting rules, segments, experiments, metrics, Node/React SDK, bootstrap, streaming"
Split.io
"Split.io: feature delivery platform with feature flags, targeting, experimentation, traffic allocation, and metrics integration"
Statsig
"Statsig: feature gates, dynamic config, experiments/A/B tests, metrics, layers, Next.js SDK, server/client evaluation"