Betterflag
Back to blog

Feature Flags in Next.js App Router

How to use feature flags in Next.js App Router: Server Components, middleware, edge evaluation, caching, and client components without killing performance.

Mehdi
August 19, 2026
#feature-flags#nextjs#tutorial#sdk#react

Feature flags in Next.js collide with the thing Next is good at: deciding as much as possible at build time. A flag is a runtime decision. If you naively fetch on every request, you throw away caching. If you bake the flag into the build, you are back to environment variables.

The workable split: cache the config (the rules), evaluate per request (the user). Server Components and middleware are the right places. useEffect is not.

This is the App Router companion to feature flags in React. Same SDK shape, Next.js-specific constraints.

Evaluate in a Server Component

import { createBetterFlag } from "@betterflag/sdk";
const flags = createBetterFlag({
apiKey: process.env.BETTERFLAG_SDK_KEY!,
environment: process.env.VERCEL_ENV === "production" ? "production" : "staging",
});
export default async function PricingPage() {
const showV2 = await flags.isEnabled("pricing-table-v2");
return showV2 ? <PricingTableV2 /> : <PricingTableV1 />;
}

For a global on/off, you do not even need a user id. For a percentage rollout or targeting, pass one:

import { cookies } from "next/headers";
export default async function PricingPage() {
const userId = (await cookies()).get("bf_uid")?.value ?? "anonymous";
const showV2 = await flags.isEnabled("pricing-table-v2", { userId });
return showV2 ? <PricingTableV2 /> : <PricingTableV1 />;
}

Set bf_uid on first visit (anonymous) and replace it on login so assignment stays sticky. Hashing on x-forwarded-for will clump whole offices into one bucket.

Caching: do not opt the whole site into dynamic

await flags.isEnabled() uses fetch or a comparable network call under the hood. In the App Router, that can mark the route dynamic.

Two ways to stay fast:

1. Revalidate the snapshot. If your SDK uses fetch, set a revalidate window so Next caches the flag config for, say, 30 seconds. A flag flip propagates within that window. That is the right trade for marketing pages and most SaaS UI.

export const revalidate = 30;

2. Local / edge evaluation. The SDK downloads the config once and evaluates in-process. Then the hot path is a hash and a boolean, not an HTTP call. This is what you want on checkout. Betterflag evaluates at the edge, under 100ms, so the "fetch the config" part is not your origin's problem.

What not to do: export const dynamic = "force-dynamic" on the root layout because one page has a flag. Scope dynamic rendering to the routes that need per-user variation.

Middleware: flags that change the request

Use middleware when the flag should affect routing or headers before React renders:

  • Maintenance mode (maintenance on → rewrite to /maintenance)
  • A gated path (beta-dashboard off → 404 or redirect)
  • Locale or theme experiments that need a cookie set early
import { NextRequest, NextResponse } from "next/server";
import { createBetterFlag } from "@betterflag/sdk";
const flags = createBetterFlag({
apiKey: process.env.BETTERFLAG_SDK_KEY!,
environment: "production",
});
export async function middleware(request: NextRequest) {
const userId = request.cookies.get("bf_uid")?.value ?? "anonymous";
const maintenance = await flags.isEnabled("maintenance-mode", { userId });
if (maintenance && !request.nextUrl.pathname.startsWith("/maintenance")) {
return NextResponse.rewrite(new URL("/maintenance", request.url));
}
return NextResponse.next();
}

Keep middleware flags few. Middleware runs on a lot of requests. A kill switch for a broken path belongs here. A button color does not.

Client components

When a leaf client component needs a flag, pass the boolean in as a prop from the Server Component parent. That is the boring, correct path.

export default async function Page() {
const showV2 = await flags.isEnabled("editor-v2");
return <EditorClient showV2={showV2} />;
}

If a client subtree must evaluate on its own (a widget on a mostly-static page), use a provider seeded with a snapshot from the server, same as the React bootstrap pattern. Do not fetch in useEffect.

Client evaluation keys are NEXT_PUBLIC_* and can only read. Agent keys, management tokens, and anything that can flip a flag stay on the server.

Environments

Map Vercel environments to flag environments explicitly:

| Vercel | Flag environment | |---|---| | development | development | | preview | staging (or a per-PR env if you have them) | | production | production |

Do not target on window.location.hostname to fake this. Preview URLs change. A dedicated staging environment with pricing-table-v2 on, production off, is the whole point of splitting environments.

Testing

Force overrides in Playwright or Vitest so CI is not coupled to production flags.

process.env.BETTERFLAG_OVERRIDES = "pricing-table-v2=true";

Or wrap the SDK in tests. Hit both variants. The off path is what you will serve when you kill the flag; if it 500s, the kill switch is a no-op.

A Next.js checklist

  • Server Component or middleware, not useEffect.
  • Cache the config (revalidate or local evaluation). Do not force-dynamic the site.
  • Sticky user id in a cookie for percentage rollouts.
  • Preview deploys use the staging flag environment.
  • Client keys evaluate only.
  • Both variants tested.

That is the App Router version. The React-only version is how to add feature flags in React. The product version is Betterflag: one meter, edge evaluation, MCP so Claude Code can create pricing-table-v2 without a dashboard tab. Pricing starts at $9.99/mo. Alpha waitlist members lock in 50% off for life.

FAQ

How do I use feature flags in Next.js App Router?
Evaluate flags in a Server Component, in middleware, or at the edge, then pass the boolean into the tree. Do not fetch flags in a client useEffect. For mostly-static pages, cache the flag snapshot with a short revalidate window so you stay fast without rebuilding to change a flag.
Should I evaluate feature flags in Next.js middleware or in a Server Component?
Middleware if the flag changes routing, headers, or whether a request is rewritten (maintenance mode, a new locale, a gated path). Server Components if the flag changes what a page renders. Client components only for purely interactive UI that cannot be decided on the server.
Do feature flags break static rendering in Next.js?
They can, if you opt the whole route into dynamic rendering for a boolean. Cache the flag config with revalidate (ISR-style) or evaluate at the edge from a cached snapshot so the page can stay static for users who share a variation. User-specific flags need some dynamic work; global on/off flags do not.
Where should the Next.js feature flag SDK key live?
The evaluation key for server and edge can live in environment variables. The client evaluation key, if you need one, is a NEXT_PUBLIC_ value that can only evaluate, never write. Management and agent keys never go in the browser.