By following this guide, you will build a working feature flag system inside a Next.js 15 app that lets you toggle features on or off per user segment — without a redeployment. The setup takes roughly 90 minutes and uses tools your team likely already has: Vercel Edge Config, a lightweight flag evaluation layer, and React context. As of 2026, feature flags are a standard delivery practice at companies shipping software continuously, and this tutorial shows you how to implement them without a third-party SaaS bill.
What You'll Build
- A flag configuration layer stored in Vercel Edge Config that updates in under 300 ms globally
- A server-side flag resolver that evaluates flags per request using Next.js 15 App Router middleware
- A React context provider that exposes flag values to any client component without prop drilling
- A percentage-based rollout rule so you can safely release to 10 % of users before going live
- A local development override pattern so engineers can test flagged features without touching production config
Prerequisites
- Node.js 20+ and pnpm 9 installed locally
- A Next.js 15 project using the App Router (TypeScript recommended)
- A Vercel account — the free Hobby tier is sufficient
- Basic familiarity with Next.js middleware and React context
Step 1: Set Up Vercel Edge Config as Your Flag Store
Edge Config is Vercel's globally replicated key-value store. Reads happen at the edge in under 1 ms because data is co-located with every edge node. It is purpose-built for configuration values that need to update instantly without a redeploy — exactly what feature flags require.
How do you create an Edge Config store?
In your Vercel dashboard, go to Storage → Edge Config → Create. Name it feature-flags. Vercel will give you a connection string that looks like this:
EDGE_CONFIG=https://edge-config.vercel.com/ecfg_xxxxxxxxxxxx?token=your-token
Add this to your .env.local file and to your Vercel project's environment variables. Then install the SDK:
pnpm add @vercel/edge-config
Now seed your first flags via the Vercel dashboard under the Edge Config item, or use the REST API:
curl -X PATCH \\
"https://api.vercel.com/v1/edge-config/{configId}/items" \\
-H "Authorization: Bearer YOUR_VERCEL_TOKEN" \\
-H "Content-Type: application/json" \\
-d '{
"items": [
{ "operation": "upsert", "key": "new_dashboard", "value": { "enabled": true, "rollout": 10 } },
{ "operation": "upsert", "key": "beta_checkout", "value": { "enabled": false, "rollout": 0 } }
]
}'
Common pitfall: Edge Config is read-only at runtime from your app. Never try to write flags from your Next.js server — all writes must go through the Vercel API or dashboard.
Step 2: Build the Flag Resolver Utility
Create a single module that reads flags from Edge Config and evaluates rollout percentages. Centralising this logic means you change behaviour in one file, not scattered across route handlers.
// lib/flags.ts
import { get } from '@vercel/edge-config';
export type FlagConfig = {
enabled: boolean;
rollout: number; // 0-100
};
export type FlagName = 'new_dashboard' | 'beta_checkout';
/**
* Evaluates a single feature flag for a given user ID.
* Returns true if the flag is enabled AND the user falls within the rollout percentage.
*/
export async function isFlagEnabled(
flagName: FlagName,
userId: string
): Promise {
try {
const config = await get(flagName);
if (!config || !config.enabled) return false;
if (config.rollout >= 100) return true;
if (config.rollout <= 0) return false;
// Deterministic hash so the same user always gets the same result
const hash = await deterministicHash(userId + flagName);
return hash % 100 < config.rollout;
} catch {
// Fail open — never block users if Edge Config is unavailable
return false;
}
}
async function deterministicHash(input: string): Promise {
const encoder = new TextEncoder();
const data = encoder.encode(input);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = new Uint8Array(hashBuffer);
// Use the first 4 bytes as an unsigned 32-bit integer
return (hashArray[0] * 16777216 + hashArray[1] * 65536 + hashArray[2] * 256 + hashArray[3]) % 100;
}
Why use a deterministic hash instead of Math.random()?
A random value changes on every request, meaning the same user sees different experiences on every page load. A SHA-256 hash of the user ID plus the flag name is stable across requests and servers, so a user in Sydney and a user in Toronto get a consistent experience without a database lookup.
Step 3: Evaluate Flags in Next.js Middleware
Next.js 15 middleware runs on the edge before any route renders. This is the right place to evaluate flags and attach results to request headers, so both server components and client components can access them without extra round-trips.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { isFlagEnabled } from './lib/flags';
export async function middleware(request: NextRequest) {
const userId = request.cookies.get('user_id')?.value ?? 'anonymous';
const [newDashboard, betaCheckout] = await Promise.all([
isFlagEnabled('new_dashboard', userId),
isFlagEnabled('beta_checkout', userId),
]);
const response = NextResponse.next();
response.headers.set('x-flag-new-dashboard', String(newDashboard));
response.headers.set('x-flag-beta-checkout', String(betaCheckout));
return response;
}
export const config = {
matcher: ['/((?!_next|favicon.ico).*)'],
};
Pro tip: Evaluate all flags in a single Promise.all call. Each isFlagEnabled call reads from the same locally cached Edge Config replica, so parallelising adds almost no latency overhead while keeping your middleware fast.
Common pitfall: Do not import heavy Node.js modules into middleware. The edge runtime does not support the full Node.js API. The @vercel/edge-config package is edge-compatible by design.
Step 4: Expose Flags to Client Components via React Context
Server components can read flag headers directly using headers() from next/headers. Client components need a context provider. Set it up once in your root layout and every component in the tree gains access.
// context/flags-context.tsx
'use client';
import { createContext, useContext } from 'react';
export type Flags = {
new_dashboard: boolean;
beta_checkout: boolean;
};
const FlagsContext = createContext({
new_dashboard: false,
beta_checkout: false,
});
export function FlagsProvider({ flags, children }: { flags: Flags; children: React.ReactNode }) {
return {children} ;
}
export function useFlags(): Flags {
return useContext(FlagsContext);
}
// app/layout.tsx
import { headers } from 'next/headers';
import { FlagsProvider } from '@/context/flags-context';
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const headersList = await headers();
const flags = {
new_dashboard: headersList.get('x-flag-new-dashboard') === 'true',
beta_checkout: headersList.get('x-flag-beta-checkout') === 'true',
};
return (
{children}
);
}
Now any client component can gate UI like this:
// components/DashboardNav.tsx
'use client';
import { useFlags } from '@/context/flags-context';
export function DashboardNav() {
const { new_dashboard } = useFlags();
return new_dashboard ? : ;
}
Step 5: Add Local Development Overrides
Engineers need to test flagged features without touching production Edge Config. Add a .env.local-based override layer that bypasses Edge Config entirely during local development.
// lib/flags.ts — updated isFlagEnabled
export async function isFlagEnabled(
flagName: FlagName,
userId: string
): Promise {
// Local override: FLAG_new_dashboard=true in .env.local
const envKey = `FLAG_${flagName}`;
if (process.env[envKey] !== undefined) {
return process.env[envKey] === 'true';
}
// ... rest of Edge Config logic unchanged
}
Add this to your .env.local:
# Force-enable new dashboard for all local requests
FLAG_new_dashboard=true
FLAG_beta_checkout=false
Pro tip: Commit a .env.local.example file with all flag overrides set to false so new engineers on your team have a working template on day one. Teams at Lenka Studio use this pattern across every Next.js project with feature flags to keep local environments predictable.
Step 6: Test the Rollout Logic
Write a unit test to confirm that the hash-based rollout behaves as expected. Use Vitest (covered separately in the React component testing guide on this blog).




