# flags sdk v0.1

The Flags SDK adapter.

@dif.sh/flags-sdk is an adapter for the Flags SDK, the flags package used by most Next.js apps. Your flag() declarations resolve from the same .md files and the same generated client as the rest of dif, and evaluating a flag still makes no network call.

Reach for it when the app already declares flags with flags/next or flags/sveltekit, or when you want flags to show up in Vercel's Flags Explorer. If you don't use the Flags SDK, call dif() or assign() directly instead — the typed client is the shorter path.

The package exports two things:

Install

$npm install flags @dif.sh/flags-sdk @dif.sh/sdk
$npm install -D @dif.sh/cli
// note@dif.sh/sdk is a peer dependency, and your install must hold exactly one copy of it. The generated client registers experiments into a module-level registry inside the SDK, and the adapter reads that same registry — a second nested copy resolves to an empty one, and every flag falls back to its defaultValue.

Declare flags

A flag is a .md file like any other dif experiment. Scaffold the workspace and draft one:

$npx @dif.sh/cli init
$npx @dif.sh/cli new new-checkout --surface checkout

Declare the variants and set status: active. The first variant is the fallback; raise the second one's weight toward 100 to roll the flag out:

# dif/experiments/active/new-checkout.md
id: new-checkout
status: active
owner: ada@acme.dev
surface: checkout
variants:
  - id: "off"
    weight: 90
  - id: "on"
    weight: 10
metrics:
  primary: completed_checkout

dif build compiles the active files into dif/generated/client.ts, which dif init gitignores. Regenerate it before every build:

// package.json
"scripts": { "prebuild": "dif build" }

Import the generated client once in flags.ts, then declare the flags. The flag key is the experiment id:

// flags.ts
import "./dif/generated/client";
import { flag } from "flags/next";
import { difAdapter } from "@dif.sh/flags-sdk";

export const newCheckoutFlag = flag<boolean>({
  key: "new-checkout",
  defaultValue: false,
  adapter: difAdapter.isEnabled(),
});

export const heroCtaFlag = flag<string>({
  key: "home-hero-cta",
  defaultValue: "control",
  adapter: difAdapter.variant(),
});
  • difAdapter.variant() resolves to the assigned variant id, such as control or variant_a.
  • difAdapter.isEnabled() resolves to true when the assigned variant is not the first declared one. difAdapter.isEnabled({ on: "variant_b" }) is true only for variant_b.

Then call the flag where you render:

// app/page.tsx
import { newCheckoutFlag } from "@/flags";

export default async function Page() {
  const newCheckout = await newCheckoutFlag();
  return newCheckout ? <OnePageCheckout /> : <Checkout />;
}

Adapter options

difAdapter is a default instance. Use createDifAdapter to change its defaults:

// flags.ts
import { createDifAdapter } from "@dif.sh/flags-sdk";

const difAdapter = createDifAdapter({
  overrides: process.env.VERCEL_ENV !== "production",
  origin: (key) =>
    `https://github.com/acme/app/blob/main/dif/experiments/active/${key}.md`,
});
optiondefaultnotes
cookieNamedif_uidCookie the default identify reads the user id from.
overridestrueHonor the _dif preview cookie. dif qa --user u_1 --force new-checkout=on prints the matching ?_dif= link.
onExposure—Called with { key, variant, bucket, userId } when a request is bucketed. See exposures.
origin—URL the Flags Explorer links each flag to — point it at the .md file.

Identify users

identify runs during rendering, where cookies can't be set, so mint the anonymous id in middleware. Setting it on the forwarded request lets the first render read it; setting it on the response persists it:

// middleware.ts — on Next.js 16, name the file proxy.ts and export `proxy`
import { type NextRequest, NextResponse } from "next/server";
import { DIF_OVERRIDES_COOKIE, DIF_UID_COOKIE } from "@dif.sh/flags-sdk";

export function middleware(request: NextRequest) {
  const uid = request.cookies.get(DIF_UID_COOKIE)?.value ?? crypto.randomUUID();
  const force = request.nextUrl.searchParams.get(DIF_OVERRIDES_COOKIE);

  // forward on the request so identify() sees them during this render
  request.cookies.set(DIF_UID_COOKIE, uid);
  if (force !== null) request.cookies.set(DIF_OVERRIDES_COOKIE, force);

  const response = NextResponse.next({ request: { headers: request.headers } });
  response.cookies.set(DIF_UID_COOKIE, uid, {
    path: "/", sameSite: "lax", maxAge: 60 * 60 * 24 * 365,
  });
  return response;
}

To bucket signed-in users on their account id, or to pass the attributes an experiment's audience block checks, supply your own identify:

// flags.ts
const identify = dedupe((async ({ headers, cookies }) => ({
  userId: session?.userId ?? cookies.get(DIF_UID_COOKIE)?.value ?? null,
  attributes: { plan: session?.plan ?? "free" },
})) satisfies Identify<DifEntities>);
// noteThe default identify passes no attributes, so an experiment with an audience block always serves its fallback until you provide one. Attribute names must match the audience_attributes declared in dif/config.yaml.

Fallbacks

For a flag whose variants are off and on:

requestvariant()isEnabled()exposure
user id set, audience matchesbucketed variantvariant !== "off"fires
no user id"off"false—
audience miss, or exclusion-group loss"off"false—
_dif preview forceforced varianttrue—
Flags Explorer overrideoverride valueoverride value—
id missing from the generated clientdecide throws → defaultValue—

When decide throws, the Flags SDK returns the flag's defaultValue and logs the error; a flag declared without one rejects. The message names the fix: import ./dif/generated/client in flags.ts, run dif build, and check the file is status: active.

Exposures

dif analyses an experiment from exposure events: one per user bucketed into a variant. The adapter assigns on the server and fires no exposure of its own, so pass onExposure to record them:

// flags.ts
import { waitUntil } from "@vercel/functions";

const difAdapter = createDifAdapter({
  onExposure: (exposure) => waitUntil(sendToAnalytics("dif.exposure", exposure)),
});
  • It runs once per flag per request and keeps no state across requests. Deduplicate on (key, userId) in your pipeline.
  • It isn't awaited. Errors are logged and never change the flag value, so wrap network calls in waitUntil.
  • Next.js renders prefetched routes, so an exposure can fire for a page the visitor never opens.
// noteThere is no server-side delivery path to dif.sh Cloud yet: DifServer sends metrics, not exposures. Forward onExposure to your own pipeline, or record the exposure in the browser with recordExposure() from @dif.sh/sdk.

Flags Explorer

getProviderData emits every registered experiment for Vercel's Flags Explorer. Importing flags.ts also imports the generated client, which fills the registry it reads. Passing the flags exports gives isEnabled() flags false/true options instead of variant ids:

// app/.well-known/vercel/flags/route.ts
import { createFlagsDiscoveryEndpoint } from "flags/next";
import { getProviderData } from "@dif.sh/flags-sdk";
import * as flags from "../../../../flags";

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

Each definition's description carries the experiment's surface, variant weights, fallback variant, and exclusion group.

invariantthe adapter never evaluates a flag over the network: it reads dif/generated/client.ts, so a flag resolves from the commit that's deployed.