All skills
vercel-labs avatar

/flags-sdk

@ce31fdf official

Set up and use feature flags and A/B tests with the Flags SDK (`flags` npm package) and Vercel Flags. Use when installing or configuring the SDK, adding a new or existing flag, wiring `vercelAdapter` (OIDC or SDK keys), declaring flags with `flag()`, using the `vercel flags` CLI (create, inspect, list, enable, disable, set, update, split, rollout, rules, segments, use-targeting, evaluations, versions, open, archive, unarchive, rm, sdk-keys, override, prepare), setting up providers/adapters (Vercel, Statsig, LaunchDarkly, PostHog, GrowthBook, Global Config, OpenFeature, Split, Flagsmith, Reflag, Optimizely, or custom), precompute, `identify`/`dedupe`, Flags Explorer/Toolbar, Next.js or SvelteKit, or encrypting flag values. Triggers: feature flags, feature gates, A/B testing, experimentation, gradual rollout, traffic split, targeting rules, flag overrides, precompute, Flags Explorer, Vercel Flags, vercel flags CLI, `flags/next`, `flags/sveltekit`, `flags/react`, `@flags-sdk/*`.

  • 11 files
  • 168.6 KB
  • Updated 13 hours ago
  • GitHub

Use this Skill: https://skilld.dev/gh/vercel-labs/vercel-plugin/flags-sdk

This session only. Nothing lands on disk.

upstreamreferencesnextjs.md

≈3.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Next.js Integration

Table of Contents

Quickstart

pnpm i flags

Declare a flag in flags.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:
pnpm i @vercel/toolbar
  1. Add Next.js plugin:
// 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);
  1. Render toolbar in root layout:
// 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:

// 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:

// 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:

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):

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:

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. Reuse existing values; generate one only for an environment where it is absent.

Step 1: Create flag group

// 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

// 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

// 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

// 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:

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

Or with labels:

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:

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

File tree:

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

Pages Router precompute

// 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:

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:

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

// 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

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

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:

// 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:

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 steps first.

App Router

// 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:

module.exports = {
  async rewrites() {
    return [
      {
        source: '/.well-known/vercel/flags',
        destination: '/api/vercel/flags',
      },
    ];
  },
};
// 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);
}

Source: SKILL.md on GitHub

No third-party reports yet.

Signed by skilld at ce31fdf. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 9 hours ago.

Activeupdated 13 hours ago
Other metadata
summary
Flags SDK guidance — declare flags with flag(), connect provider adapters, manage Vercel Flags via the vercel flags CLI, precompute static variants, and set up the Flags Explorer.
metadata
{
  "priority": 6,
  "docs": [
    "https://vercel.com/docs/flags",
    "https://flags-sdk.dev"
  ],
  "sitemap": "https://vercel.com/sitemap.xml",
  "pathPatterns": [
    "flags.ts",
    "flags.tsx",
    "lib/flags.ts",
    "src/flags.ts",
    "lib/flags/**",
    "src/flags/**",
    ".well-known/vercel/flags/**"
  ],
  "importPatterns": [
    "flags/next",
    "flags/sveltekit",
    "flags/react",
    "flags",
    "@flags-sdk/*",
    "@vercel/flags"
  ],
  "bashPatterns": [
    "\\bvercel\\s+flags\\b",
    "\\bvc\\s+flags\\b",
    "\\bnpm\\s+(install|i|add)\\s+[^\\n]*\\bflags\\b",
    "\\bpnpm\\s+(install|i|add)\\s+[^\\n]*\\bflags\\b",
    "\\bbun\\s+(install|i|add)\\s+[^\\n]*\\bflags\\b",
    "\\byarn\\s+add\\s+[^\\n]*\\bflags\\b",
    "\\bnpm\\s+(install|i|add)\\s+[^\\n]*@flags-sdk/",
    "\\bpnpm\\s+(install|i|add)\\s+[^\\n]*@flags-sdk/",
    "\\bbun\\s+(install|i|add)\\s+[^\\n]*@flags-sdk/",
    "\\byarn\\s+add\\s+[^\\n]*@flags-sdk/"
  ],
  "promptSignals": {
    "phrases": [
      "feature flag",
      "feature flags",
      "flags sdk",
      "vercel flags",
      "flags explorer",
      "feature gate",
      "feature gating",
      "a/b test",
      "a/b testing",
      "ab test",
      "ab testing",
      "flag rollout",
      "gradual rollout",
      "percentage rollout",
      "kill switch",
      "flag variant",
      "flag adapter",
      "precompute flags",
      "traffic split",
      "targeting rules",
      "flag rules",
      "flag segments",
      "flag targeting",
      "flag evaluations",
      "flag overrides",
      "existing flag"
    ],
    "allOf": [
      [
        "flag",
        "rollout"
      ],
      [
        "flag",
        "variant"
      ],
      [
        "flag",
        "toggle"
      ],
      [
        "flag",
        "experiment"
      ],
      [
        "experiment",
        "variant"
      ],
      [
        "split",
        "test"
      ]
    ],
    "anyOf": [
      "flag",
      "experiment",
      "experimentation",
      "rollout",
      "variant"
    ],
    "noneOf": [
      "command-line flag",
      "command line flag",
      "cli flag",
      "compiler flag",
      "flag emoji",
      "rolling release"
    ],
    "minScore": 6
  }
}
retrieval
{
  "aliases": [
    "feature flags",
    "feature toggles",
    "vercel flags",
    "flags sdk",
    "a/b testing"
  ],
  "intents": [
    "add a feature flag",
    "run an a/b test",
    "gate a feature",
    "roll out gradually",
    "manage flags from cli"
  ],
  "entities": [
    "Flags SDK",
    "Vercel Flags",
    "Flags Explorer",
    "vercelAdapter",
    "precompute",
    "FLAGS_SECRET"
  ]
}
chainTo
[
  {
    "pattern": "precompute\\s*\\(|generatePermutations|flags/next.*precompute",
    "targetSkill": "routing-middleware",
    "message": "Precompute pattern detected — loading Routing Middleware guidance for the middleware rewrites that serve static flag variants."
  },
  {
    "pattern": "@flags-sdk/(edge|global)-config|create(Edge|Global)ConfigAdapter",
    "targetSkill": "vercel-storage",
    "message": "Global Config flag adapter detected — loading Vercel storage guidance for Global Config setup and limits."
  }
]

README badge

README badge for vercel-labs/vercel-plugin/flags-sdk