Feature Flags in Next.js: Server-Side Evaluation Without the Hydration Flash

Add feature flags to a Next.js app: evaluate on the server in the App Router or getServerSideProps to kill the hydration flash, then hydrate the client for live updates.

  • nextjs
  • tutorials

Feature flags in a single-page React app are a client-side state problem. Feature flags in Next.js are that, plus a rendering problem the SPA never had: the server renders HTML before the browser has loaded a single flag. Evaluate flags only in the browser and the server paints the default variant, the page hydrates, then the real variant swaps in a beat later. For a button color that is invisible. For gating a whole checkout flow or a paid-tier feature it is a visible flash, and React will warn you about the hydration mismatch in the console.

The fix is to evaluate where the markup is generated. This guide adds feature flags to a Next.js app with the right SDK in the right place: the Node server SDK for server-side evaluation in the App Router and getServerSideProps, and the React SDK for live client updates, wired so the server-rendered value and the first client render always agree. The code covers both the App Router and the Pages Router.

Key Takeaways

  • The Next.js-specific problem is the hydration flash: a browser-only flag read paints the default variant on the server, then swaps after flags load. Evaluating on the server puts the correct variant in the initial HTML.
  • Use two SDKs for two jobs: @featureflip/node (a server key, local evaluation) inside Server Components and getServerSideProps, and @featureflip/react (a client key) for live updates in Client Components.
  • FeatureflipClient.get() dedupes by key, so a module-level server client is safe to import from every request handler without opening duplicate connections.
  • To keep a flag live without re-introducing the flash, pass the server-evaluated value as the default to useFeatureFlag('key', serverValue). The hook returns its default until the client connects, so the first client render matches the server HTML, then streaming takes over.
  • The server key is read in process and never ships to the browser; only the NEXT_PUBLIC_ client key reaches the bundle, and it returns pre-evaluated results, not your ruleset.

1. The flash a Next.js app has that a React SPA doesn’t

In a client-rendered React app, the first paint already happens in the browser, so reading a flag with a hook and gating on a loading state is the whole story. Next.js renders on the server first. That server pass runs before any browser code, so a flag read that only works in the browser has nothing to return yet and falls back to its default.

Walk through what the user sees. The server renders the page with new-checkout defaulting to false and ships HTML for the legacy flow. The browser parses that HTML, hydrates, mounts the Featureflip client, and fetches flags. A few hundred milliseconds later the client learns this user is in the rollout and re-renders the new flow. The user watched the old checkout for a moment and then saw it replaced. Worse, the HTML React rendered on the server (legacy) does not match what the client wants to render on its first pass (new), which is exactly the condition that produces a React hydration warning.

Time until the correct variant is on screen A timeline comparing two approaches. With client-only evaluation, the page paints the default variant first and shows a flash of the wrong variant until the browser SDK loads flags, then swaps to the correct one. With server evaluation, the correct variant is in the initial HTML, so there is no flash. Time until the correct variant is on screen Illustrative: first paint to the correct variant, one flag-gated region Client-only eval flash of wrong variant correct Server eval correct from first paint first paint client flags load
Client-only evaluation paints the default first, so a flag whose default differs from the user's variant flashes until the browser loads flags. Evaluating on the server puts the correct variant in the initial HTML.

The plain React approach of gating on useFeatureflipStatus().isReady and showing a skeleton solves the flash, but in Next.js it means server-rendering a skeleton instead of real content, which throws away the main reason you reached for server rendering. The better answer is to evaluate the flag while the server is building the page.


2. Two SDKs, two jobs

Featureflip ships a server SDK and a client SDK, and a Next.js app uses both because it runs code in two places.

The Node server SDK (@featureflip/node) holds your full ruleset in memory and evaluates flags locally. A boolVariation() call is a synchronous in-memory read, not a network request, so evaluating during a server render adds no per-request round trip. It authenticates with a server SDK key, which stays in your server process.

The React SDK (@featureflip/react) runs in the browser, opens a streaming connection, and re-renders components when a flag changes while a user sits on the page. It authenticates with a client key, which is safe to ship in the bundle because client evaluation returns only the results a user is allowed to see, never the ruleset.

The division of labor is simple. Evaluate on the server to get correct HTML on the first paint. Add the client SDK only where you also want a flag to update live without a reload. Many flags need only the first one.

Terminal window
npm install @featureflip/node # server-side evaluation
npm install @featureflip/react @featureflip/browser # client-side live updates

3. App Router: evaluate a flag in a Server Component

Create one shared server client. FeatureflipClient.get() dedupes by SDK key and create() waits for the first flag load, so memoizing the promise at module scope gives you a single initialized client for the whole process, safe to import anywhere.

lib/featureflip.ts
import { FeatureflipClient } from '@featureflip/node';
let clientPromise: Promise<FeatureflipClient> | null = null;
export function getFlags() {
clientPromise ??= FeatureflipClient.create({
sdkKey: process.env.FEATUREFLIP_SDK_KEY!,
});
return clientPromise;
}

Now an async Server Component awaits the client and reads the flag for the current user. Evaluation takes a context, so pass whatever identifies the user from your session.

app/checkout/page.tsx
import { getFlags } from '@/lib/featureflip';
import { getSession } from '@/lib/session';
export default async function CheckoutPage() {
const flags = await getFlags();
const session = await getSession();
const newCheckout = flags.boolVariation(
'new-checkout',
{ user_id: session.userId },
false,
);
return newCheckout ? <NewCheckout /> : <LegacyCheckout />;
}

The HTML that leaves the server already contains the correct flow. There is no skeleton, no hydration warning, and nothing to swap once the browser takes over. If new-checkout is driven by a percentage rollout or a targeting rule on the user’s plan, the server resolves it the same way the client would, because both run the same evaluation logic against the same context.

Because the variation call is a local read, you can sprinkle these through the tree without worrying about request waterfalls. The one async step is awaiting getFlags() once; every boolVariation after that is synchronous.


4. Pages Router: evaluate in getServerSideProps

The Pages Router has no Server Components, but the pattern is the same: evaluate inside getServerSideProps, where you already have access to the request, and pass the result down as a prop.

pages/checkout.tsx
import type { GetServerSideProps } from 'next';
import { getFlags } from '../lib/featureflip';
import { getSession } from '../lib/session';
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
const flags = await getFlags();
const session = await getSession(req);
const newCheckout = flags.boolVariation(
'new-checkout',
{ user_id: session.userId },
false,
);
return { props: { newCheckout } };
};
export default function CheckoutPage({ newCheckout }: { newCheckout: boolean }) {
return newCheckout ? <NewCheckout /> : <LegacyCheckout />;
}

The page renders on the server with the flag already resolved, so the markup is correct before it reaches the browser. This is the same getFlags() singleton from the App Router example, which is why keeping the client in one module pays off: both routers share it.


5. Keep a flag live without bringing the flash back

Server evaluation gives you correct HTML. What it does not give you is live updates: a flag flipped in the dashboard will not reach an already-open page, because the server only ran once, at request time. For most flags that is fine. For an operational kill switch or a flag you flip during an incident, you want the open page to react.

The instinct is to add the client provider and read the flag with a hook, but a naive hook read brings the flash straight back, because the hook returns its default until the browser SDK finishes loading. The fix is to seed the hook with the value the server already computed.

Put the provider in a Client Component near the root and pass it the same context you used on the server:

app/flags-provider.tsx
'use client';
import { FeatureflipProvider } from '@featureflip/react';
export function FlagsProvider({
userId,
children,
}: {
userId: string;
children: React.ReactNode;
}) {
return (
<FeatureflipProvider
clientKey={process.env.NEXT_PUBLIC_FEATUREFLIP_CLIENT_KEY!}
context={{ user_id: userId }}
>
{children}
</FeatureflipProvider>
);
}

Then read the flag in a Client Component, passing the server value as the default:

app/checkout/checkout-toggle.tsx
'use client';
import { useFeatureFlag } from '@featureflip/react';
export function CheckoutToggle({ serverValue }: { serverValue: boolean }) {
const newCheckout = useFeatureFlag('new-checkout', serverValue);
return newCheckout ? <NewCheckout /> : <LegacyCheckout />;
}

The Server Component evaluates the flag and hands the result down:

const newCheckout = flags.boolVariation('new-checkout', { user_id: session.userId }, false);
return <CheckoutToggle serverValue={newCheckout} />;

Now the timing works out. The server renders with the real value. On the client, useFeatureFlag returns serverValue until the browser SDK connects, so the first client render matches the server HTML exactly, with no mismatch and no flash. Once the streaming connection is live, a later flip of new-checkout pushes to the open page and the component re-renders on its own. You get a correct first paint and live updates, which is the combination a browser-only setup cannot reach.


6. Server keys, client keys, and where evaluation runs

Two keys do two things, and keeping them straight is the security model.

The server SDK key authorizes local evaluation of the full ruleset. Read it from an un-prefixed environment variable like FEATUREFLIP_SDK_KEY so Next.js keeps it on the server and never inlines it into client JavaScript. The client key authorizes pre-evaluated reads for one user and belongs in a NEXT_PUBLIC_FEATUREFLIP_CLIENT_KEY variable, which Next.js deliberately exposes to the browser. Shipping the client key is intended; shipping the server key is not.

One runtime caveat. The Node SDK uses Node.js APIs, so run it where the Node.js runtime is available, which is the default for Server Components, Route Handlers, and getServerSideProps. Next.js middleware runs on the Edge runtime, which does not provide the full Node API surface, so do flag evaluation in a Node.js-runtime route rather than inside middleware. If you need a flag at the edge for routing, call the evaluation API over HTTP from there and keep local evaluation in your Node.js code paths.


7. Why a first-party SDK instead of a fetch in getServerSideProps

You can skip the SDK and call an HTTP endpoint from the server on every request. It works until the endpoint is slow or down, and now every page render waits on a network call or has to handle that failure. Local evaluation sidesteps that: the SDK loads the ruleset once, keeps it current over a stream, and every boolVariation is an in-memory read. The same logic that makes a server-side flag read cheap is the logic you would otherwise hand-build and keep in sync with the dashboard, which is the kind of undifferentiated work a build-versus-buy decision is meant to settle.

Pricing matters for a server-rendered app in a specific way. Server evaluation runs once per request and client evaluation runs once per visitor, so a usage-billed platform meters both your traffic and your render volume, and a launch that triples visits triples the bill. Featureflip uses flat pricing with unlimited evaluations on every plan, so neither server renders nor browser sessions move the invoice. The pricing page has the numbers, and the comparison pages show how that lands against per-MAU and per-seat vendors for a high-traffic Next.js site.


The shorter version

The thing that makes feature flags in Next.js different from a React SPA is the server render: evaluate a flag only in the browser and the server paints the default, producing a flash and a hydration mismatch. Evaluate on the server instead. Use @featureflip/node with a server key inside a Server Component or getServerSideProps, reading flags with boolVariation('key', context, default) as cheap in-memory calls. That alone gives correct HTML on the first paint for any flag that does not need to change on an open page. When a flag does need to update live, add the @featureflip/react provider in a Client Component and pass the server-evaluated value as the hook’s default, so the first client render matches the server HTML and the streaming connection takes over from there. Keep the server key un-prefixed and the client key behind NEXT_PUBLIC_, and run the Node SDK in Node.js-runtime routes rather than Edge middleware.


Frequently asked questions

How do I add feature flags to a Next.js app?

Install @featureflip/node and evaluate flags on the server. Create one client with FeatureflipClient.create({ sdkKey: process.env.FEATUREFLIP_SDK_KEY }), memoized at module scope, then call flags.boolVariation('your-flag', { user_id }, false) inside an async Server Component (App Router) or inside getServerSideProps (Pages Router). The page renders with the correct variant already in the HTML. Add the @featureflip/react provider only for flags you also want to update live in the browser.

Why do feature flags flash on the screen in Next.js?

Because the server renders before any browser code runs. A flag that is only read in the browser has no value during the server pass, so it falls back to its default, and Next.js ships HTML for that default. When the browser loads flags a moment later, the real variant replaces the default, which both looks like a flash and triggers a React hydration mismatch warning. Evaluating the flag on the server with the Node SDK puts the correct variant in the initial HTML and removes both symptoms.

Do I use the React SDK or the Node SDK in Next.js?

Both, for different jobs. The Node SDK (@featureflip/node) evaluates flags on the server in Server Components and getServerSideProps, which is what kills the hydration flash. The React SDK (@featureflip/react) runs in the browser and gives you live updates over a streaming connection for flags that change while a user is on the page. For a flag that never changes on an open page, the Node SDK alone is enough.

How do I avoid a hydration mismatch when a flag updates live?

Evaluate the flag on the server and pass that value as the default to the client hook: useFeatureFlag('key', serverValue). The hook returns its default until the browser SDK connects, so the first client render uses the same value the server rendered and the markup matches. After the client connects over streaming, it updates the value live. Seeding the hook with the server value is what keeps the first render consistent while still allowing live changes.

Can I evaluate flags in Next.js middleware or the Edge runtime?

The Node SDK targets the Node.js runtime, which is the default for Server Components, Route Handlers, and getServerSideProps, so do local evaluation there. Next.js middleware runs on the Edge runtime, which lacks the full Node API surface, so the Node SDK is not the right tool inside middleware. If you need a flag for edge routing, call the evaluation API over HTTP from the middleware and keep local SDK evaluation in your Node.js routes.


Featureflip is a focused, flat-priced feature flag platform with a first-party Node SDK for server-side evaluation and a React SDK for live client updates, so a Next.js app gets correct HTML on the first paint and real-time flags after, without metering either on traffic. The Node SDK reference and the companion guides on feature flags in React and running feature flags in a Node.js backend have the full API, and you can start on the free Solo plan without a credit card.