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.