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).