Feature flags as Code

Property-based testing is a powerful technique for verifying code behavior, but it's often perceived as complex and difficult to integrate into existing projects. Vercel's Flags SDK aims to change that by providing a lightweight, provider-agnostic layer for feature flags in Next.js and SvelteKit applications.

The SDK isn't a competitor to existing feature flag services. Instead, it sits between your application and your flag source—whether that's a provider, a database, or simple static values—and enforces best practices to keep your pages fast. Below we walk through the core principles, starting with a basic flag and progressing to more nuanced configurations.

Starting simple

Install the SDK with npm install flags, then declare a flag:

flags.ts

import { flag } from "flags/next";

export const showBanner = flag({

key: "banner",

decide: () => false,

});

The exported showBanner function can now be called in any server context:

app/page.tsx

import { showBanner } from "../flags";

export default async function Page() {

const banner = await showBanner();

return (

<div>

{banner ? <Banner /> : null}

{/* other components */}

</div>

);

}

Because the decide function always returns false, the banner stays hidden. This is the foundational pattern: a function that resolves a value based on the current request.

Core design principles

The SDK differs from typical provider SDKs in a few intentional ways:

Flags are functions

Modeling each flag as its own function allows the underlying implementation to change without touching call sites. It also means your editor's "Find All References" works to locate every usage, making cleanup and migration easier.

Flags evaluate only on the server

Client-side flag evaluation has inherent problems: layout shifts, jank, and the risk of exposing sensitive data. To avoid these, the SDK ensures flags are always resolved server-side, with only the final value passed to the client.

Server-side evaluation doesn't inherently require dynamic rendering. The SDK supports precomputing, generating multiple static variants of a page and using Edge Middleware to route responses. This keeps pages fully static—ideal for marketing experiments and useful with Partial Prerendering.

Flags accept no arguments

The decide function receives no arguments from the call site. All information it needs must be gathered from the request context using Next.js primitives like headers() and cookies(). Expensive data fetching can be deduplicated with React's cache.

Because every call site provides identical input, all reads of a flag within a request reliably return the same value. This lets you reason about a flag's output by looking solely at its decide function. It also makes it trivial to swap between flag providers one flag at a time. When you delete a flag, all data it depended on disappears with it—leaving no orphaned context or unused requests.

The primitive flag

const showBanner = false;

Even a constant variable can function as a feature flag when changed and redeployed. The value could originate from a file, an environment variable, a database, or a provider. It could be uniform for all traffic or derived from the request—regional targeting, or checking team membership after validating an authorization token.

Flag progressions and tradeoffs

Let's examine how evolving from basic flags to more complex setups shifts the balance between speed, control, and risk.

Basic static flag

flags.ts

import { flag } from "flags/next";

export const showBanner = flag({

key: "banner",

decide: () => false,

});

With a hardcoded decide, turning the flag on means editing code and redeploying. The upsides are immediate, latency-free resolution and perfect availability. Git history tracks every change.

But there are real downsides: changes require deploys, existing deployments retain old values, and the same value applies to every visitor—no gradual rollouts.

Toolbar overrides

With the Vercel Toolbar connected (in production and local development), you can view and override flags through an authenticated endpoint at /.well-known/vercel/flags.

Toolbar overrides work because the Vercel Toolbar sets an encrypted cookie, vercel-flag-overrides, containing flag names and their overridden values. This prevents arbitrary injection. With the Flags SDK, overrides are honored automatically, so a flag hardcoded to false in production can still be temporarily turned on for your own session.

This opens a few practical workflows.

Safe releases: Merge your feature without enabling it, then use an override in a production session to verify behavior before a full rollout.

Trunk-based development: Merge small branches continuously into main while features remain hidden behind flags. This reduces long-running branches and minimizes merge conflicts.

End-to-end tests: The encrypt function can generate valid override cookies programmatically for your test suite, making multi-scenario tests straightforward.

Environment variables

Shifting from hardcoded values to an environment variable adds another layer of flexibility:

export const showBanner = flag({

key: "banner",

decide: () => process.env.SHOW_BANNER === "1",

});

This mirrors the hardcoded approach in speed and availability, but lets you enable flags per deployment context—say, all preview deployments on a branch—without touching code.

The tradeoff remains that changing the variable requires a redeployment, so it's not suited for instant global rollbacks. That's where a centralized store like Edge Config comes in.

Moving Flags to an External Data Store

Up to this point, every flag change has required a redeploy. Making the decide function asynchronous changes that — it can now read from any data source, such as a database or external configuration service.

Vercel's Edge Config is built specifically for this purpose. It uses active global replication to colocate configuration with compute, which means reads from Vercel-hosted apps typically take under 1ms p90, and updates propagate in under 10s. Because Vercel manages deployments, it can propagate config data alongside them, which is why most Edge Config reads never even cross the network.

With a project connected to an Edge Config holding the relevant data, you can wire the two together:

{

"flags": {

"banner": true,

"sale": false

}

}

Using Edge Config's get function, the flag definition reads the flags key:

flags.ts

import { flag } from “flags/next”;

import { get } from “@vercel/edge-config”;

export const showBanner = flag({

key: “banner”,

defaultValue: false,

async decide() {

// educational example, use @flags-sdk/edge-config for real applications

const flags = await get(“flags”);

return flags?.banner;

}

});

The above example is illustrative but inefficient — it triggers a separate Edge Config read for every flag. The Edge Config Provider optimizes this by reading the underlying config once per request instead:

flags.ts

import { flag } from 'flags/next';

import { edgeConfigAdapter } from '@flags-sdk/edge-config';

export const showBanner = flag({

// Will get the `example-flag` key from the `flags` object

key: 'banner',

// Will load the `flags` key from Edge Config

adapter: edgeConfigAdapter(),

});

With this setup, editing Edge Config updates the flag value without a redeploy. You can change values via the API endpoint or the editor:

Updates take effect globally in seconds, and since Edge Config logs every change, you retain history and can restore previous values quickly. The tradeoff: decide becomes async, introducing a small amount of latency. For this use case, that cost is negligible — and it buys the ability to kill a feature or time a launch without waiting on a build.

Integrating Feature Flag Providers

If you already use a feature flag provider, you can call it directly from the decide function. Most providers follow a singleton pattern where you must await the client's init before reading data:

// flags.ts

import { flag } from "flags/next";

import { statsigAdapter } from "@flags-sdk/statsig";

export const showBanner = flag({

key: "banner",

adapter: statsigAdapter.featureGate((gate) => gate.value),

})‌;‍‌‌‌‍‌‌‍‍‌‌‍‍​‍​‍​‍‍‌‍‌‍‍‌‌‌‍‌‌‌‍‍‌‌‌‍‌‍‌‌‌‌‍‌‍‌‍‌‌‌‌‍‌‍​‍​‍​‍‍‌‍​‌‌‌‍‍‌‌‍​‍‌‍‌‍‌‍‌‌‍‌‌‌‍‌‌‍‍‌‌‌‌‍‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‍‍‌‌‍‍‌‍‌‌‌‌‌‌‍​‌‍‍‌‍‍‌‌‍‌‌‌‌‌‍‌‌‌‌‌‌‍‌‌‌

There is a catch with provider SDKs: they are designed for long-running servers, where the initial network request to load flag configurations happens once and updates occur in the background. In serverless environments, function instances spin up and down frequently — Edge Middleware instances often live for seconds, not minutes. Cold instances must fetch flag configuration quickly to keep latency acceptable.

To address this, Vercel partners with providers like Statsig, Hypertune, LaunchDarkly, and Split. Their integrations synchronize flag configurations into Edge Config, allowing your application to bootstrap the provider SDK from there — bypassing the initial network request entirely.

OpenFeature Compatibility

If you're wondering how the Flags SDK relates to OpenFeature, the two work together rather than competing. The Flags SDK OpenFeature adapter provides values in a vendor-agnostic way, so you can swap providers while retaining the SDK's framework integration and pre-computation pattern.

Choosing the Right Tradeoff per Flag

The Flags SDK lets you pick the appropriate tradeoff for each flag without refactoring the code that consumes it. A flag can begin as a hardcoded boolean, graduate to a rollout or experiment, then settle into an operational toggle backed by an environment variable after a successful launch.

The SDK is available at flags-sdk.dev with full documentation on getting started.