Tutorial

Using flags in Next.js

Learn how to use the @vercel/flags/next submodule for feature flags in Next.js App Router.
Table of Contents

The flags pattern and precomputed flags pattern exported from @vercel/flags/next are experimental. We are still actively researching and have further iterations planned. These exports are not covered by semantic versioning as indicated by the unstable_ prefix.

The @vercel/flags package exposes the @vercel/flags/next submodule, which implements the Feature Flags pattern.

  1. Run the following code in your terminal to generate a secret and save it as the FLAGS_SECRET environment variable:

    Terminal
    node -e "console.log(crypto.randomBytes(32).toString('base64url'))"

    This secret is used by the SDK to automatically read overrides set by the Vercel Toolbar.

  2. Declare a feature flag called showSummerSale in code. The decide function can be async and is called when the feature flag is used. In this example, it always returns false, so this feature flag would always be off:

    flags.ts
    import { unstable_flag as flag } from '@vercel/flags/next';
     
    export const showSummerSale = flag({
      key: 'summer-sale',
      decide: () => false,
    });
  3. You can now import the flags file that you created in the previous step, and call the showSummerSale flag in code. This will run the decide function and forward its return value:

    app/page.tsx
    import { showSummerSale } from '../flags';
     
    export default async function Page() {
      const sale = await showSummerSale();
      return sale ? <p>discounted</p> : <p>regular price</p>;
    }

Feature flags can be backed by any feature flag provider or no feature flag provider at all.

In the initial implementation of decide, we always return false. However, your team members can use Vercel Toolbar to override this feature flag.

This can be particularly useful for trunk-based development, where you merge new features to the main branch frequently while keeping the feature flag off. You can still use Vercel Toolbar to create an override and try out the feature in all development environments, including production.

As the requirements for this feature flag change you might want to change it to be backed by an Environment Variable. You can then change the decide function like so:

flags.ts
import { unstable_flag as flag } from '@vercel/flags/next';
 
export const showSummerSale = flag({
  key: 'summer-sale',
  decide: () => process.env.SUMMER_SALE_ENABLED === '1',
});

This now allows changing the feature flag without changing code: change the environment variable and redeploy. So far the feature flag always loaded instantly, since the resolution logic is hardcoded. But the downside of this approach is that changing a feature flag will not affect any existing deployments or preview deployments of other branches.

For further iterations, the feature flag can be backed by Edge Config in order to be changed without redeploying and will affect all deployments:

flags.ts
import { unstable_flag as flag } from '@vercel/flags/next';
import { get } from '@vercel/edge-config';
 
export const showSummerSale = flag({
  key: 'summer-sale',
  async decide() {
    const value = await get('summer-sale'); // could use this.key instead
    return value ?? false;
  },
});

Finally, you could back it by a feature flag provider:

flags.ts
import { unstable_flag as flag } from '@vercel/flags/next';
 
export const showSummerSale = flag({
  key: 'summer-sale',
  async decide() {
    return getLaunchDarklyClient().variation(this.key, false);
  },
});

If you set an override for this feature flag using Vercel Toolbar, then the feature flag will respect the override and will not invoke the decide function.

This approach calls reportValue whenever a feature flag is called. This works even when a flag is overridden.

It is not possible to pass any arguments when invoking the feature flag. This constraint was introduced on purpose to ensure a consistent return value for the given feature flag throughout the whole application. Instead, if you need to get some context based on the incoming request you can manually call cookies() or headers(). You can pair this with React.cache which caches for the duration the request being handled, so you avoid fetching the same value from multiple feature flags.

Flags automatically trigger dynamic mode since they are, by definition, dynamic.

Feature flags can be used in Edge Middleware to influence your application's behavior. You can simply call the feature flag just like you would in React Server Components. It's important to keep in mind that Edge Middleware runs globally, so we advise to keep the decide functions fast. Otherwise a single slow feature flag used in Edge Middleware will end up delaying the whole response.

To solve this problem we built Edge Config. Edge Config is a globally distributed datastore which can be read from Edge Middleware in under 1ms (p90).

For marketing pages, it is common to A/B test two versions of the page. Traditionally, this either required rendering the page dynamically or using client-side JavaScript to manipulate the page contents. These approaches have downsides like increased latency or visual jank.

By combining the power of Edge Middleware, Edge Config, and feature flags you can achieve a better setup that allows the page itself to stay completely static. For this approach to work, you would create two static versions of the marketing page you want to experiment on: version A ( app/home-a/page.tsx) and version B (app/home-b/page.tsx). You can then use Edge Middleware to rewrite the incoming request either to version A or version B. The feature flag used in Edge Middleware would decide which version to show, and you would rewrite to the correct one so it gets delivered at the edge at extremely low latency due to its static nature.

Image showing how Edge Middleware rewrites the request

In its most simple form, the decision of which user should see which variant can be made using a random number.

flags.ts
import { unstable_flag as flag } from '@vercel/flags/next';
 
export const showSummerSale = flag({
  key: 'summer-sale',
  decide: () => Math.random() > 0.5,
});
middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
import { showSummerSale } from './flags.ts';
 
export const config = { matcher: ['/'] };
 
export async function middleware(request: NextRequest) {
  const summerSale = await showSummerSale();
 
  // Determine which version to show based on the feature flag
  const version = summerSale ? '/home-b' : '/home-a';
 
  // Rewrite the request to the appropriate version
  const nextUrl = new URL(version, request.url);
 
  return NextResponse.rewrite(nextUrl);
}

This is a trivial example, but you could extend the feature flag's decide function to use Edge Config. You can learn more in the Flags as Code blog post. However, this approach ends up with combinatory explosion as soon as more than one feature flag is used on each page. For those scenarios we have a more advanced pattern explained in the next section.

This is an extension to the previously described pattern. It allows combining multiple feature flags on a single, static page.

This pattern is useful for experimentation on static pages, as it allows middleware to make routing decisions, while being able to keep the different variants of the underlying flags static.

It further allows generating a page for each combination of feature flags either at build time or lazily the first time it is accessed. It can then be cached using ISR so it does not need to be regenerated.

Technically this works by using dynamic route segments to transport an encoded version of the feature flags computed within Edge Middleware. Encoding the values within the URL allows the page itself to access the precomputed values, and also ensures there is a unique URL for each combination of feature flags on a page. Because the system works using rewrites, the visitor will never see the URL containing the flags. They will only see the clean, original URL.

  1. You can export one or multiple arrays of flags to be precomputed. This by itself does not do anything yet, but you will use the exported array in the next step:

    flags.ts
    import { unstable_flag as flag } from '@vercel/flags/next';
     
    export const showSummerSale = flag({
      key: 'summer-sale',
      decide: () => false,
    });
     
    export const showBanner = flag({
      key: 'banner',
      decide: () => false,
    });
     
    // a group of feature flags to be precomputed
    export const marketingFlags = [showSummerSale, showBanner] as const;
  2. In this step, import marketingFlags from the flags file that you created in the previous step. Then, call precompute with the list of flags to be precomputed. You'll then forward the precomputation result to the underlying page using an URL rewrite:

    middleware.ts
    import { type NextRequest, NextResponse } from 'next/server';
    import { unstable_precompute as precompute } from '@vercel/flags/next';
    import { marketingFlags } from './flags';
     
    // Note that we're running this middleware for / only, but
    // you could extend it to further pages you're experimenting on
    export const config = { matcher: ['/'] };
     
    export async function middleware(request: NextRequest) {
      // precompute returns a string encoding each flag's returned value
      const code = await precompute(marketingFlags);
     
      // rewrites the request to include the precomputed code for this flag combination
      const nextUrl = new URL(
        `/${code}${request.nextUrl.pathname}${request.nextUrl.search}`,
        request.url,
      );
     
      return NextResponse.rewrite(nextUrl, { request });
    }
  3. Next, import the feature flags you created earlier, such as showBanner, while providing the code from the URL and the marketingFlags list of flags used in the precomputation.

    When the showBanner flag is called within this component it reads the result from the precomputation, and it does not invoke the flag's decide function again:

    app/[code]/page.tsx
    import { marketingFlags, showSummerSale, showBanner } from '../../flags';
     
    export default async function Page({ params }: { params: { code: string } }) {
      // access the precomputed result by passing params.code and the group of
      // flags used during precomputation of this route segment
      const summerSale = await showSummerSale(params.code, marketingFlags);
      const banner = await showBanner(params.code, marketingFlags);
     
      return (
        <div>
          {banner ? <p>welcome</p> : null}
     
          {summerSale ? (
            <p>summer sale live now</p>
          ) : (
            <p>summer sale starting soon</p>
          )}
        </div>
      );
    }

    This approach allows middleware to decide the value of feature flags and to pass the precomputation result down to the page. This approach also works with API Routes.

  4. So far you've set up middleware to decide the value of each feature flag to be precomputed and to pass the value down. In this step you can enable ISR to cache generated pages after their initial render:

    app/[code]/layout.tsx
    import type { ReactNode } from 'react';
     
    export async function generateStaticParams() {
      // returning an empty array is enough to enable ISR
      return [];
    }
     
    export default async function Layout({ children }: { children: ReactNode }) {
      return children;
    }
  5. The @vercel/flags/next submodule exposes a helper function for generating pages for different combinations of flags at build time. This function is called generatePermutations and takes a list of flags and returns an array of strings representing each combination of flags:

    app/[code]/layout.tsx
    import type { ReactNode } from 'react';
    import { unstable_generatePermutations as generatePermutations } from '@vercel/flags/next';
     
    export async function generateStaticParams() {
      const codes = await generatePermutations(marketingFlags);
      return codes.map((code) => ({ code }));
    }
     
    export default async function Layout({ children }: { children: ReactNode }) {
      return children;
    }

    You can further customize which specific combinations you want render by passing a filter function as the second argument of generatePermutations.

  6. It is also possible to use the layout file we created earlier to surface precomputed values. They will then get picked up by the Vercel Toolbar, and they can be used to annotate Web Analytics.

    app/[code]/layout.tsx
    import type { ReactNode } from 'react';
    import { marketingFlags } from '../flags';
    import { encrypt } from '@vercel/flags';
    import { unstable_deserialize as deserialize } from '@vercel/flags/next';
    import { FlagValues } from '@vercel/flags/react';
    import { Suspense } from 'react';
     
    export async function generateStaticParams() {
      return [];
    }
     
    export default async function Layout({
      children,
      params,
    }: {
      children: ReactNode;
      params: { code: string };
    }) {
      const values = await deserialize(marketingFlags, params.code);
     
      return (
        <>
          {children}
          <Suspense fallback={null}>
            <FlagValues values={await encrypt(values)} />
          </Suspense>
        </>
      );
    }

This approach of precomputing feature flags and using middleware to rewrite users to a statically generated version of a page eliminates layout shift, loading spinners, and flashing the wrong experiment.

Last updated on September 19, 2024