Updated: August 29, 2026
The Vercel Flags SDK is a framework-aware feature flag library that lets Next.js 16 evaluate flags on the server, in proxy.ts, and inside React Server Components without leaking bundles to the client. You install flags, declare each flag as a function that returns a value, and call it wherever you need it. The SDK handles caching, deduping across a request, and hooking into the Vercel Toolbar for overrides. I’ve moved three production apps onto it, and it replaces roughly 80% of what a paid flag vendor gave us (without the invoice).
What is the Vercel Flags SDK?
The Vercel Flags SDK is an open-source TypeScript library, published as flags on npm, that gives web frameworks a first-class API for feature flags. Vercel donated the reference site to flags-sdk.dev in 2025, and the package is now maintained alongside the Next.js repo. It is provider-agnostic: the SDK itself only defines how a flag is declared, evaluated, and reported to the toolbar. Where the flag values come from (Edge Config, LaunchDarkly, Statsig, a Postgres table, or a hard-coded object) is a swappable adapter.
What makes it interesting for staff-eng readers is the runtime story. Flags are evaluated on the server. There’s no client-side script that phones home, no flash of the wrong variant, no bundle bloat from a giant SDK. In a Server Component you call myFlag() the same way you’d call any async function, and the returned value is a plain string, boolean, or object. The SDK caches the result inside React’s request memoization, so evaluating the same flag from a nested layout costs you nothing.
The other differentiator is precomputation. Traditional flag systems make every page dynamic because you can’t cache a page whose HTML depends on user identity. The Flags SDK’s precompute() helper turns the set of active variants into a short hex code that you append to the URL in proxy.ts, letting ISR and Partial Prerendering keep serving cached HTML per cohort.
Installing and configuring flags in Next.js 16
The install is a single package plus one environment variable. I’ll assume you’re on Next.js 16 with the App Router; the SDK also supports 14 and 15, but the proxy.ts integration below needs 16.
pnpm add flags
# For Edge Config storage:
pnpm add @vercel/edge-configThe one required env var is FLAGS_SECRET, a 32-byte base64 string the SDK uses to sign the encrypted payload it sends to the Vercel Toolbar. Generate it once and add it to .env.local, then to your Vercel project settings.
node -e "console.log(crypto.randomBytes(32).toString('base64url'))"
# paste into .env.local
FLAGS_SECRET=…Create flags.ts at the project root. Every flag is a call to flag() that returns a typed function. Keep the file small and boring. The point is that rg "flag(" tells you every live flag in the codebase.
// flags.ts
import { flag } from 'flags/next';
export const newCheckoutFlag = flag<boolean>({
key: 'new-checkout',
description: 'Enables the redesigned checkout flow',
defaultValue: false,
decide: async () => {
// Replace with an adapter call in the next section.
return false;
},
});
export const pricingVariantFlag = flag<'control' | 'anchored' | 'discount'>({
key: 'pricing-variant',
description: 'A/B/C test for the pricing page',
defaultValue: 'control',
options: ['control', 'anchored', 'discount'],
decide: async () => 'control',
});Then wire up the Flags Discovery Endpoint so the Vercel Toolbar can list your flags on preview deployments. This is a one-line route handler; skip it and overrides silently fail.
// app/.well-known/vercel/flags/route.ts
import { verifyAccess, type ApiData } from 'flags';
import { getProviderData } from 'flags/next';
import { NextResponse, type NextRequest } from 'next/server';
import * as flags from '@/flags';
export async function GET(request: NextRequest) {
const access = await verifyAccess(request.headers.get('Authorization'));
if (!access) return NextResponse.json(null, { status: 401 });
const providerData: ApiData = getProviderData(flags);
return NextResponse.json(providerData);
}Deploy once, open a preview URL, and you’ll see the flags listed in the Vercel Toolbar with per-flag overrides that persist in an encrypted cookie for your session only. This is the workflow I hand to product managers. They can toggle variants on preview URLs without touching my code or the flag store, which honestly cut about half of the “can you turn X on for me real quick” Slacks I used to get.
How do feature flags work in Server Components?
A flag is a function. In a Server Component, you call it, await the result, and render accordingly. No context provider, no hook, no hydration boundary. The SDK memoizes the decide function per request using React’s cache(), so calling newCheckoutFlag() from three nested components hits your Edge Config exactly once.
// app/checkout/page.tsx
import { newCheckoutFlag } from '@/flags';
import LegacyCheckout from './legacy-checkout';
import NewCheckout from './new-checkout';
export default async function CheckoutPage() {
const useNew = await newCheckoutFlag();
return useNew ? <NewCheckout /> : <LegacyCheckout />;
}For Client Components, you have two choices. Read the flag in the nearest Server Component parent and pass the resolved value down as a prop. That’s what I do 90% of the time because it keeps the client bundle clean. If you genuinely need the value in a deeply-nested client tree, expose it through headers() or a Server Action, but don’t reach for a client-side SDK just to avoid drilling a prop.
Server Actions and Route Handlers work identically. Because flag() returns a plain async function, there’s nothing framework-specific about calling it. Use it wherever you’d use any other side-effectful helper.
Using Edge Config as the flag store
Edge Config is Vercel’s replicated read-only key-value store designed for exactly this use case: config that’s read on every request but changes rarely. Reads are pulled from a per-region cache maintained by the edge network and typically resolve in single-digit milliseconds, which is why the Flags SDK ships a first-class adapter for it.
Provision an Edge Config store from the Vercel dashboard (Storage → Create → Edge Config), then wire the adapter into your flag declarations.
// flags.ts
import { flag } from 'flags/next';
import { createEdgeConfigAdapter } from '@flags-sdk/edge-config';
const edgeConfig = createEdgeConfigAdapter(process.env.EDGE_CONFIG!);
export const newCheckoutFlag = flag<boolean>({
key: 'new-checkout',
description: 'Enables the redesigned checkout flow',
defaultValue: false,
adapter: edgeConfig(),
});The stored payload is a JSON object keyed by flag name. The adapter reads flags.new-checkout from the Edge Config item and returns it. Update the value in the Vercel dashboard or with the @vercel/edge-config SDK, and every server region picks it up within a few seconds without a redeploy.
If you need percentage rollouts, evaluate them inside the decide function using the visitor’s stable identifier, usually a cookie you set in proxy.ts. I cover the pattern in the Next.js middleware and proxy guide. The SDK exposes an identify option that receives the request headers and returns an object you can use inside decide, which keeps the decide function pure and testable.
A/B testing with precomputation and static routes
The classic problem with flags in a JAM stack: reading cookies inside a page turns the page dynamic, and dynamic pages don’t benefit from ISR or the static shell in Partial Prerendering. Precomputation is the escape hatch.
The idea is that instead of evaluating flags inside the page, you evaluate them once in proxy.ts and encode the result as a short hex code appended to the URL. The router then rewrites to /pricing/<code>, and Next.js sees each cohort as a distinct static path that can be generated at build time or on demand via generateStaticParams().
// proxy.ts
import { NextResponse, type NextRequest } from 'next/server';
import { precompute } from 'flags/next';
import { pricingVariantFlag } from './flags';
const marketingFlags = [pricingVariantFlag];
export async function middleware(request: NextRequest) {
if (request.nextUrl.pathname !== '/pricing') return NextResponse.next();
const code = await precompute(marketingFlags);
const url = new URL(`/pricing/${code}${request.nextUrl.search}`, request.url);
return NextResponse.rewrite(url, { request });
}
export const config = { matcher: '/pricing' };// app/pricing/[code]/page.tsx
import { pricingVariantFlag } from '@/flags';
const marketingFlags = [pricingVariantFlag];
export default async function Page({
params,
}: {
params: Promise<{ code: string }>;
}) {
const { code } = await params;
const variant = await pricingVariantFlag(code, marketingFlags);
return <PricingHero variant={variant} />;
}The route now has three physical URLs, one per variant, and each one is a fully static page that ISR or PPR can cache independently. The user never sees the code in their browser because proxy.ts uses rewrite, not redirect. This is the pattern I run for our marketing site’s pricing page, and it holds a 99+ Lighthouse score while running a three-way price test.
For details on how PPR interacts with cached segments, see the Next.js 16 Cache Components guide.
Rollout patterns: percentages, cohorts, and kill switches
The SDK doesn’t bake rollout logic in. It gives you a place to put it. Here are the three patterns that cover 95% of what I need in production.
Percentage rollout with a stable visitor ID
Set a first-party visitor_id cookie in proxy.ts if one doesn’t exist, then hash it inside the decide function and bucket the visitor into a 0–99 range.
import { flag } from 'flags/next';
import { cookies } from 'next/headers';
async function bucket(visitorId: string): Promise<number> {
const buf = new TextEncoder().encode(visitorId);
const hash = await crypto.subtle.digest('SHA-256', buf);
return new DataView(hash).getUint32(0) % 100;
}
export const newCheckoutFlag = flag<boolean>({
key: 'new-checkout',
defaultValue: false,
identify: async () => ({
visitorId: (await cookies()).get('visitor_id')?.value ?? 'anon',
}),
decide: async ({ entities }) => {
const rolloutPct = 25; // read this from Edge Config in real code
return (await bucket(entities.visitorId)) < rolloutPct;
},
});Cohort targeting
Any information you can read on the server can gate a flag: plan tier from the session, geo from request.geo, user role from the JWT. Put the read in identify and the decision in decide. Keep them separate, because identify runs once per request and its output is what gets sent to the toolbar for debugging.
Kill switches
Every risky flag gets a boolean sibling I call <flag>-kill. If the kill flag is on, the decide function returns the safe default regardless of the rollout percentage or cohort. I learned this the hard way after a rollout percentage change in Edge Config took ~15 seconds to propagate to every region, which was 15 seconds too many at 3 AM. A dedicated kill flag lets me flip one boolean and reason about exactly what happens.
Vercel Flags SDK vs LaunchDarkly, Statsig, and GrowthBook
The SDK isn’t a competitor to the paid vendors. It’s a shim that fits over them. You can point the Flags SDK adapter at LaunchDarkly, Statsig, GrowthBook, Hypertune, or Split, and get server-side evaluation with the same flag() declaration syntax. What varies is what the vendor gives you on top of raw flag storage: analytics, experiment stats, audit logs, and RBAC.
| Feature | Flags SDK + Edge Config | LaunchDarkly | Statsig | GrowthBook |
|---|---|---|---|---|
| Server-side eval in Next.js | Native | Via adapter | Via adapter | Via adapter |
| Edge/proxy.ts support | Yes | Yes | Yes | Yes |
| Storage cost (small team) | Free (Edge Config) | ~$0 up to 1k MAU, then paid | Free tier: 1M events/mo | Self-host free; cloud paid |
| Experiment stats | Bring your own | Included | Included (strong) | Included |
| Audit log / RBAC | Vercel roles only | Included | Included | Included |
| Vendor lock-in risk | Low (open-source SDK) | High | Medium | Low (Apache 2.0) |
| Best for | Vercel-hosted apps, dev-owned flags | Enterprise with compliance needs | Product teams doing heavy A/B testing | Teams wanting self-host + Bayesian stats |
My rule of thumb: if flags live inside engineering and the decide logic is code, use the SDK against Edge Config and skip the vendor bill. The moment non-engineering stakeholders need audit trails, approval workflows, or experiment result dashboards, add a vendor and keep the SDK as the abstraction layer. That way you don’t rewrite your call sites when you switch vendors, only the adapter changes.
Pitfalls I have hit shipping flags at scale
Six things caught me in the first three months of running the SDK in production. If you skim nothing else, skim this list.
- Missing Flags Discovery Endpoint. The toolbar silently doesn’t show your flags. There is no error. Verify by hitting
/.well-known/vercel/flagsin a preview and confirming you get JSON. - Flag files imported by client components. If
flags.tsimports server-only code (likecookies), importing it from a Client Component throws at build time in Turbopack. Split intoflags.server.tsandflags.client.ts, or add'server-only'to the top of the flag module. - Precompute + auth cookies. If your
identifyfunction reads an auth cookie and the cookie value has high cardinality (a user ID rather than a bucket), precompute generates a unique code per user and blows out your ISR cache. Bucket first, precompute the bucket. (I hit this exact bug shipping a personalization test last spring; the CDN hit-rate dropped from 92% to about 4% overnight and it took a full afternoon to trace back.) - Forgetting
defaultValue. If the Edge Config item is missing the key and you didn’t setdefaultValue, the flag returnsundefined. Ternary with an undefined boolean surprises people. - Overusing runtime flags in monorepos. In a Turborepo, if you evaluate the same flag in ten packages via ten different helper functions, you lose the request-scoped memoization. Import the flag from a single shared package (
@repo/flags) and re-export. This is exactly the pattern the Turborepo shared-packages guide recommends for other cross-cutting utilities. - Runtime mismatch. If your flag adapter uses Node APIs but
proxy.tsruns in the edge runtime, evaluations fromproxy.tswill crash. Pick edge-compatible adapters (Edge Config is one) or move flag reads out ofproxy.ts. Read Edge Runtime vs Node Runtime for the compatibility matrix.
Frequently Asked Questions
Is the Vercel Flags SDK free?
Yes. The flags npm package is MIT-licensed and free to use with any hosting provider. If you use Edge Config as the storage backend on Vercel, the free tier includes 8 KB per store and 15 stores per account, which fits dozens of flags. Third-party adapters (LaunchDarkly, Statsig, etc.) follow their own pricing.
Do I need Vercel hosting to use the Flags SDK?
No. The SDK itself is provider-neutral and runs on any Node.js host. You lose the built-in Vercel Toolbar integration and Edge Config, but you can still declare flags, evaluate them server-side, and point the adapter at LaunchDarkly, Statsig, a database, or an in-memory object.
Can I evaluate feature flags in proxy.ts (middleware)?
Yes, and this is the recommended pattern for URL-level rewrites and precomputation. Use edge-runtime-compatible adapters; Edge Config is the safest choice. Avoid adapters that require Node APIs (like the file system) because proxy.ts runs on the edge runtime by default in Next.js 16.
How is the Flags SDK different from a boolean in an env var?
An env var requires a redeploy to change, applies to every user identically, and has no audit trail. The Flags SDK evaluates per-request based on identity, updates in seconds without a redeploy via Edge Config or your vendor, and integrates with the Vercel Toolbar for per-user overrides during QA.
Does the Flags SDK work with Partial Prerendering?
Yes, when combined with precompute(). Precomputation encodes each variant combination as a URL segment so Next.js can generate and cache one static shell per cohort. Without precomputation, calling a flag inside a page turns that page fully dynamic and disables PPR’s static shell for the affected route.




