Skip to main content
Back to Blog
Tutorials
3 min read
November 8, 2024

How to Build a Feature Flag System in Next.js

Implement feature flags in Next.js. Toggle features without deploying, run A/B tests, and do gradual rollouts.

Ryel Banfield

Founder & Lead Developer

Feature flags let you toggle features without deploying new code. They enable A/B testing, gradual rollouts, and safe deployments.

Step 1: Simple Environment-Based Flags

// lib/flags.ts
export const flags = {
  newPricingPage: process.env.NEXT_PUBLIC_FLAG_NEW_PRICING === "true",
  betaDashboard: process.env.NEXT_PUBLIC_FLAG_BETA_DASHBOARD === "true",
  darkModeDefault: process.env.NEXT_PUBLIC_FLAG_DARK_MODE === "true",
  aiChatWidget: process.env.NEXT_PUBLIC_FLAG_AI_CHAT === "true",
} as const;

export type Flag = keyof typeof flags;
// Usage
import { flags } from "@/lib/flags";

export default function PricingPage() {
  if (flags.newPricingPage) {
    return <NewPricingPage />;
  }
  return <OldPricingPage />;
}

Step 2: Database-Backed Feature Flags

// db/schema.ts
export const featureFlags = pgTable("feature_flags", {
  id: uuid("id").primaryKey().defaultRandom(),
  key: text("key").notNull().unique(),
  enabled: boolean("enabled").default(false),
  percentage: integer("percentage").default(100), // For gradual rollout
  description: text("description"),
  updatedAt: timestamp("updated_at").defaultNow(),
});
// lib/flags.ts
import { db } from "@/db";
import { featureFlags } from "@/db/schema";
import { eq } from "drizzle-orm";
import { unstable_cache } from "next/cache";

export const getFlag = unstable_cache(
  async (key: string): Promise<boolean> => {
    const flag = await db
      .select()
      .from(featureFlags)
      .where(eq(featureFlags.key, key))
      .limit(1);

    if (!flag[0]) return false;
    return flag[0].enabled;
  },
  ["feature-flags"],
  { revalidate: 60 } // Cache for 60 seconds
);

export const getFlagWithRollout = unstable_cache(
  async (key: string, userId: string): Promise<boolean> => {
    const flag = await db
      .select()
      .from(featureFlags)
      .where(eq(featureFlags.key, key))
      .limit(1);

    if (!flag[0] || !flag[0].enabled) return false;
    if (flag[0].percentage === 100) return true;

    // Deterministic hash based on userId + flag key
    const hash = simpleHash(userId + key);
    return hash % 100 < flag[0].percentage;
  },
  ["feature-flags-rollout"],
  { revalidate: 60 }
);

function simpleHash(str: string): number {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    const char = str.charCodeAt(i);
    hash = (hash << 5) - hash + char;
    hash |= 0;
  }
  return Math.abs(hash);
}

Step 3: Feature Flag Component

// components/FeatureFlag.tsx
import { getFlag } from "@/lib/flags";

export async function FeatureFlag({
  flag,
  children,
  fallback,
}: {
  flag: string;
  children: React.ReactNode;
  fallback?: React.ReactNode;
}) {
  const isEnabled = await getFlag(flag);

  if (!isEnabled) return fallback ?? null;

  return <>{children}</>;
}
// Usage in a page
import { FeatureFlag } from "@/components/FeatureFlag";

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>

      <FeatureFlag flag="ai-chat-widget">
        <AIChatWidget />
      </FeatureFlag>

      <FeatureFlag
        flag="new-analytics"
        fallback={<OldAnalytics />}
      >
        <NewAnalytics />
      </FeatureFlag>
    </div>
  );
}

Step 4: Client-Side Feature Flags

// components/providers/FeatureFlagProvider.tsx
"use client";

import { createContext, useContext, type ReactNode } from "react";

type Flags = Record<string, boolean>;

const FlagContext = createContext<Flags>({});

export function FeatureFlagProvider({
  flags,
  children,
}: {
  flags: Flags;
  children: ReactNode;
}) {
  return <FlagContext.Provider value={flags}>{children}</FlagContext.Provider>;
}

export function useFlag(key: string): boolean {
  const flags = useContext(FlagContext);
  return flags[key] ?? false;
}
// Load flags in layout
import { FeatureFlagProvider } from "@/components/providers/FeatureFlagProvider";

export default async function RootLayout({ children }) {
  const flags = await getAllFlags();

  return (
    <html>
      <body>
        <FeatureFlagProvider flags={flags}>
          {children}
        </FeatureFlagProvider>
      </body>
    </html>
  );
}
// Use in client components
"use client";

import { useFlag } from "@/components/providers/FeatureFlagProvider";

export function NavBar() {
  const showBetaBadge = useFlag("beta-badge");

  return (
    <nav>
      <Link href="/">Home</Link>
      {showBetaBadge && (
        <span className="ml-2 rounded bg-yellow-100 px-1.5 py-0.5 text-xs font-medium text-yellow-700">
          Beta
        </span>
      )}
    </nav>
  );
}

Step 5: Admin Panel for Managing Flags

// app/api/admin/flags/route.ts
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/db";
import { featureFlags } from "@/db/schema";
import { eq } from "drizzle-orm";
import { revalidateTag } from "next/cache";

export async function GET() {
  const flags = await db.select().from(featureFlags);
  return NextResponse.json(flags);
}

export async function PATCH(req: NextRequest) {
  const { key, enabled, percentage } = await req.json();

  await db
    .update(featureFlags)
    .set({ enabled, percentage, updatedAt: new Date() })
    .where(eq(featureFlags.key, key));

  revalidateTag("feature-flags");

  return NextResponse.json({ success: true });
}

Step 6: Using Vercel Edge Config (Instant Updates)

pnpm add @vercel/edge-config
// lib/flags.ts
import { get } from "@vercel/edge-config";

export async function getFlag(key: string): Promise<boolean> {
  const value = await get<boolean>(key);
  return value ?? false;
}

Edge Config updates propagate globally in milliseconds — no deploy needed.

Step 7: A/B Testing with Feature Flags

// middleware.ts
import { NextRequest, NextResponse } from "next/server";

export function middleware(req: NextRequest) {
  // Assign user to a variant (sticky via cookie)
  let variant = req.cookies.get("ab-variant")?.value;

  if (!variant) {
    variant = Math.random() < 0.5 ? "control" : "treatment";
    const res = NextResponse.next();
    res.cookies.set("ab-variant", variant, {
      maxAge: 60 * 60 * 24 * 30, // 30 days
      httpOnly: true,
    });
    return res;
  }

  return NextResponse.next();
}
// In a page
import { cookies } from "next/headers";

export default async function PricingPage() {
  const cookieStore = await cookies();
  const variant = cookieStore.get("ab-variant")?.value;

  if (variant === "treatment") {
    return <NewPricingPage />;
  }
  return <CurrentPricingPage />;
}

Need Help Shipping Safely?

We build web applications with feature flags, gradual rollouts, and robust deployment pipelines. Contact us to discuss your project.

feature flagsNext.jsdeploymentA/B testingtutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles