# Next.js Integration

## Table of Contents

- [Quickstart](#quickstart)
- [Toolbar Setup](#toolbar-setup)
- [App Router](#app-router)
- [Pages Router](#pages-router)
- [Evaluation Context](#evaluation-context)
- [Dedupe](#dedupe)
- [Precompute](#precompute)
- [Dashboard Pages](#dashboard-pages)
- [Marketing Pages](#marketing-pages)
- [Proxy (Middleware)](#proxy-middleware)
- [Suspense Fallbacks](#suspense-fallbacks)

## Quickstart

```sh
pnpm i flags
```

Declare a flag in `flags.ts`:

```ts
import { flag } from 'flags/next';

export const exampleFlag = flag({
  key: 'example-flag',
  decide() {
    return Math.random() > 0.5;
  },
});
```

## Toolbar Setup

1. Install `@vercel/toolbar`:

```sh
pnpm i @vercel/toolbar
```

2. Add Next.js plugin:

```ts
// next.config.ts
import type { NextConfig } from 'next';
import createWithVercelToolbar from '@vercel/toolbar/plugins/next';

const nextConfig: NextConfig = {};

const withVercelToolbar = createWithVercelToolbar();
export default withVercelToolbar(nextConfig);
```

3. Render toolbar in root layout:

```tsx
// app/layout.tsx
import { VercelToolbar } from '@vercel/toolbar/next';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  // On Vercel, the toolbar is auto-injected in preview deployments.
  // This manual injection is only needed for local development.
  const shouldInjectToolbar = process.env.NODE_ENV === 'development';
  return (
    <html lang="en">
      <body>
        {children}
        {shouldInjectToolbar && <VercelToolbar />}
      </body>
    </html>
  );
}
```

## App Router

Call the flag function from any async server component or proxy:

```tsx
// app/page.tsx
import { exampleFlag } from '../flags';

export default async function Page() {
  const example = await exampleFlag();
  return <div>{example ? 'Flag is on' : 'Flag is off'}</div>;
}
```

## Pages Router

Pass `req` to the flag in `getServerSideProps`:

```tsx
// pages/index.tsx
import type { InferGetServerSidePropsType, GetServerSideProps } from 'next';
import { exampleFlag } from '../flags';

export const getServerSideProps = (async ({ req }) => {
  const example = await exampleFlag(req);
  return { props: { example } };
}) satisfies GetServerSideProps<{ example: boolean }>;

export default function Page({
  example,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  return <div>{example ? 'Flag is on' : 'Flag is off'}</div>;
}
```

## Evaluation Context

Use `identify` to establish who the request is for. The returned entities are passed to `decide`:

```ts
import { flag, dedupe } from 'flags/next';
import type { ReadonlyRequestCookies } from 'flags';

interface Entities {
  user?: { id: string };
}

const identify = dedupe(
  ({ cookies }: { cookies: ReadonlyRequestCookies }): Entities => {
    const userId = cookies.get('user-id')?.value;
    return { user: userId ? { id: userId } : undefined };
  },
);

export const myFlag = flag<boolean, Entities>({
  key: 'my-flag',
  identify,
  decide({ entities }) {
    return entities?.user?.id === 'user1';
  },
});
```

`identify` receives normalized `headers` and `cookies` that work across App Router, Pages Router, and Proxy.

### Custom evaluation context

Override identify at call site (use sparingly):

```ts
await exampleFlag.run({ identify: { user: { id: 'user1' } } });
await exampleFlag.run({ identify: () => ({ user: { id: 'user1' } }) });
```

## Dedupe

Wrap functions in `dedupe` to run them once per request within the same runtime:

```ts
import { dedupe } from 'flags/next';

const identify = dedupe(({ cookies }) => {
  return { user: { id: cookies.get('uid')?.value } };
});
```

Use cases:
- Prevent duplicate `identify` calls across multiple flags
- Generate consistent random IDs for anonymous visitor experiments

Not available in Pages Router.

## Precompute

Keep pages static while using feature flags. Proxy evaluates flags and encodes results into the URL.

### Prerequisites

Ensure `FLAGS_SECRET` is configured using [FLAGS_SECRET](../SKILL.md#flags_secret). Reuse existing values; generate one only for an environment where it is absent.

### Step 1: Create flag group

```ts
// flags.ts
import { flag } from 'flags/next';

export const showSummerSale = flag({
  key: 'summer-sale',
  decide: () => false,
});

export const showBanner = flag({
  key: 'banner',
  decide: () => false,
});

export const marketingFlags = [showSummerSale, showBanner] as const;
```

### Step 2: Precompute in proxy

```ts
// proxy.ts
import { type NextRequest, NextResponse } from 'next/server';
import { precompute } from 'flags/next';
import { marketingFlags } from './flags';

export const config = { matcher: ['/'] };

export async function proxy(request: NextRequest) {
  const code = await precompute(marketingFlags);
  const nextUrl = new URL(
    `/${code}${request.nextUrl.pathname}${request.nextUrl.search}`,
    request.url,
  );
  return NextResponse.rewrite(nextUrl, { request });
}
```

### Step 3: Read precomputed values in page

```tsx
// app/[code]/page.tsx
import { marketingFlags, showSummerSale, showBanner } from '../../flags';

type Params = Promise<{ code: string }>;

export default async function Page({ params }: { params: Params }) {
  const { code } = await params;
  const summerSale = await showSummerSale(code, marketingFlags);
  const banner = await showBanner(code, marketingFlags);

  return (
    <div>
      {banner && <p>welcome</p>}
      {summerSale ? <p>summer sale live</p> : <p>summer sale soon</p>}
    </div>
  );
}
```

### Step 4: Enable ISR & build time prerendering

```tsx
// app/[code]/layout.tsx
import { generatePermutations } from 'flags/next';

export async function generateStaticParams() {
  const codes = await generatePermutations(marketingFlags);
  return codes.map((code) => ({ code }));
}

export default async function Layout({ children }) {
  return children;
}
```

### Declaring options

Options enable efficient URL encoding and Flags Explorer display:

```ts
export const greetingFlag = flag<string>({
  key: 'greeting',
  options: ['Hello world', 'Hi', 'Hola'],
  decide: () => 'Hello world',
});
```

Or with labels:

```ts
export const greetingFlag = flag<string>({
  key: 'greeting',
  options: [
    { label: 'Hello world', value: 'Hello world' },
    { label: 'Hi', value: 'Hi' },
  ],
  decide: () => 'Hello world',
});
```

### Multiple groups

Avoid unnecessary permutations by creating separate flag groups per page:

```ts
export const rootFlags = [navigationFlag, bannerFlag];
export const pricingFlags = [discountFlag];
```

File tree:

```
app/[rootCode]/
  page.tsx
  pricing/[pricingCode]/
    page.tsx
```

### Pages Router precompute

```tsx
// pages/[code]/index.tsx
import { generatePermutations } from 'flags/next';

export const getStaticPaths = (async () => {
  const codes = await generatePermutations(marketingFlags);
  return {
    paths: codes.map((code) => ({ params: { code } })),
    fallback: 'blocking',
  };
}) satisfies GetStaticPaths;

export const getStaticProps = (async (context) => {
  if (typeof context.params?.code !== 'string') return { notFound: true };
  const example = await exampleFlag(context.params.code, marketingFlags);
  return { props: { example } };
}) satisfies GetStaticProps<{ example: boolean }>;
```

## Dashboard Pages

For authenticated dashboard pages, use `identify` to read user context from cookies/JWTs:

```ts
import type { ReadonlyRequestCookies } from 'flags';
import { flag, dedupe } from 'flags/next';

interface Entities {
  user?: { id: string };
}

const identify = dedupe(
  ({ cookies }: { cookies: ReadonlyRequestCookies }): Entities => {
    const userId = cookies.get('dashboard-user-id')?.value;
    return { user: userId ? { id: userId } : undefined };
  },
);

export const dashboardFlag = flag<boolean, Entities>({
  key: 'dashboard-flag',
  identify,
  decide({ entities }) {
    if (!entities?.user) return false;
    const allowedUsers = ['user1'];
    return allowedUsers.includes(entities.user.id);
  },
});
```

Usage in a page:

```tsx
export default async function DashboardPage() {
  const dashboard = await dashboardFlag();
  return <div>{dashboard ? 'New Dashboard' : 'Old Dashboard'}</div>;
}
```

## Marketing Pages

For static marketing pages with A/B tests, combine precompute with visitor ID generation:

### Visitor ID in proxy

```ts
// proxy.ts
import { precompute } from 'flags/next';
import { type NextRequest, NextResponse } from 'next/server';
import { marketingFlags } from './flags';
import { getOrGenerateVisitorId } from './get-or-generate-visitor-id';

export async function marketingProxy(request: NextRequest) {
  const visitorId = await getOrGenerateVisitorId(
    request.cookies,
    request.headers,
  );

  const code = await precompute(marketingFlags);

  return NextResponse.rewrite(
    new URL(`/examples/marketing-pages/${code}`, request.url),
    {
      headers: {
        'Set-Cookie': `marketing-visitor-id=${visitorId}; Path=/`,
        'x-marketing-visitor-id': visitorId,
      },
    },
  );
}
```

### Deduplicated visitor ID generation

```ts
import { nanoid } from 'nanoid';
import { dedupe } from 'flags/next';
import type { ReadonlyHeaders, ReadonlyRequestCookies } from 'flags';

const generateId = dedupe(async () => nanoid());

export const getOrGenerateVisitorId = async (
  cookies: ReadonlyRequestCookies,
  headers: ReadonlyHeaders,
) => {
  const cookieVisitorId = cookies.get('marketing-visitor-id')?.value;
  if (cookieVisitorId) return cookieVisitorId;

  const headerVisitorId = headers.get('x-marketing-visitor-id');
  if (headerVisitorId) return headerVisitorId;

  return generateId();
};
```

### Flag using visitor ID

```ts
const identify = dedupe(
  async ({ cookies, headers }: {
    cookies: ReadonlyRequestCookies;
    headers: ReadonlyHeaders;
  }): Promise<Entities> => {
    const visitorId = await getOrGenerateVisitorId(cookies, headers);
    return { visitor: visitorId ? { id: visitorId } : undefined };
  },
);

export const marketingAbTest = flag<boolean, Entities>({
  key: 'marketing-ab-test-flag',
  identify,
  decide({ entities }) {
    if (!entities?.visitor) return false;
    return /^[a-n0-5]/i.test(entities.visitor.id);
  },
});
```

## Proxy (Middleware)

Use flags in proxy to rewrite requests to static page variants:

```ts
// proxy.ts
import { type NextRequest, NextResponse } from 'next/server';
import { myFlag } from './flags';

export const config = { matcher: ['/example'] };

export async function proxy(request: NextRequest) {
  const active = await myFlag();
  const variant = active ? 'variant-on' : 'variant-off';
  return NextResponse.rewrite(new URL(`/example/${variant}`, request.url));
}
```

For multiple flags on one page, use the precompute pattern instead.

## Suspense Fallbacks

Combine precomputed flags with Partial Prerendering to serve matching skeletons:

```tsx
async function Example() {
  const hasAuth = await hasAuthCookieFlag();

  return (
    <Suspense fallback={hasAuth ? <AuthedSkeleton /> : <UnauthedSkeleton />}>
      <Dashboard />
    </Suspense>
  );
}
```

The `hasAuthCookieFlag` checks cookie existence without authenticating. Two shells get prerendered — one for each auth state — served statically with no layout shift.

## Flags Explorer (Next.js)

The Flags Explorer is part of the Vercel Toolbar. Before adding the discovery endpoint below, make sure the toolbar is set up by following the [Toolbar Setup](#toolbar-setup) steps first.

### App Router

```ts
// app/.well-known/vercel/flags/route.ts
import { getProviderData, createFlagsDiscoveryEndpoint } from 'flags/next';
import * as flags from '../../../../flags';

export const GET = createFlagsDiscoveryEndpoint(async () => {
  return getProviderData(flags);
});
```

### Pages Router

Requires a rewrite in `next.config.js`:

```js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/.well-known/vercel/flags',
        destination: '/api/vercel/flags',
      },
    ];
  },
};
```

```ts
// pages/api/vercel/flags.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { verifyAccess } from 'flags';

export async function handler(req: NextApiRequest, res: NextApiResponse) {
  const access = await verifyAccess(req.headers.authorization);
  if (!access) return res.status(401).json(null);

  const providerData = { /* ... */ };
  return res.status(200).json(providerData);
}
```
