Vercel Flags SDK

The Flags SDK is Vercel's way of declaring a flag once in your codebase and reading it anywhere on the server. A vs B ships an adapter for it: declare the flag with flag(), point it at the adapter, and every read is a real A vs B evaluation with targeting rules, A/B splits, holdouts, bandits, and a server-side exposure so the decision reaches your results.

Install

Shell
npm install @avsbhq/flags-adapter flags
Shell1 line

flags is a peer dependency. The adapter never imports it: it matches the adapter shape structurally, so a flags upgrade cannot break it at build time.

Declare a flag

TypeScript
// flags.tsimport { flag } from 'flags/next'import { avsbAdapter } from '@avsbhq/flags-adapter'import type { EvalContext } from '@avsbhq/flags-adapter'export const newCheckout = flag<boolean, EvalContext>({  key: 'new_checkout_flow',  adapter: avsbAdapter.booleanValue(),  defaultValue: false,  identify: ({ cookies }) => ({    kind: 'user',    key: cookies.get('uid')?.value ?? 'anonymous',  }),})
TypeScript14 lines
TypeScript React
// app/page.tsximport { newCheckout } from '../flags'export default async function Page() {  const showNewCheckout = await newCheckout()  return showNewCheckout ? <NewCheckout /> : <Checkout />}
TypeScript React7 lines

Add your SDK key

Shell
AVSB_SDK_KEY=sdk_production_ttqm0eaj4vth1krcb2xn
Shell1 line

That environment variable is the only configuration avsbAdapter needs.

Your SDK key is public
Your SDK key is a public identifier, not a secret: it is safe to ship in browser and mobile bundles, it can only fetch that environment's flag configuration and send events, and it can never read or change anything in your dashboard. Credentials covers all four A vs B credentials and which one to reach for.

With no key, the adapter logs one error naming the fix and every flag returns the defaultValue from its declaration. It never throws, so a missing key cannot take a page down.

The adapter surface

TypeScript
// docs-example: not typechecked here, because it sketches the shapes the// Flags SDK defines (Adapter, Logger), and this repo does not install flags.function createAvsbAdapter(options?: CreateAvsbAdapterOptions): AvsbAdapter/** Reads AVSB_SDK_KEY from the environment. Builds its client on first use. */const avsbAdapter: AvsbAdapterinterface AvsbAdapter {  booleanValue(): Adapter<boolean, EvalContext>  stringValue(): Adapter<string, EvalContext>  numberValue(): Adapter<number, EvalContext>  /** The fallback is required: there is no empty value for an arbitrary shape. */  jsonValue<T>(fallback: T): Adapter<T, EvalContext>  /** Which arm of the experiment this request landed in. Null when nothing was decided. */  variationKey(): Adapter<string | null, EvalContext>  /** The underlying client once ready, or null when it could not be created. */  avsbClient(): Promise<AvsbEvaluator | null>  /** Flush queued events and stop polling. */  close(): Promise<void>}interface CreateAvsbAdapterOptions {  /** Defaults to process.env.AVSB_SDK_KEY. */  sdkKey?: string  /** CDN base for datafile fetches. Default 'https://cdn.avsb.cloud'. */  cdnHost?: string  /** A client you already run, or a sync or async factory for one. */  client?: AvsbEvaluator | (() => AvsbEvaluator | Promise<AvsbEvaluator>)  /** Record an exposure per decision. Default 'auto'. */  exposure?: 'auto' | 'off'  /** Cookie carrying the anonymous visitor id. Default 'avsb_anon_id'. */  anonCookieName?: string  /** Where the Vercel Toolbar links this flag. */  origin?: string | ((flagKey: string) => string | undefined)  /** Defaults to the console at warn level. */  logger?: Logger}
TypeScript37 lines

The entities type is EvalContext, the same context object every A vs B SDK takes, so multi-context works here exactly as it does elsewhere. A rule can bucket on user.key while matching an audience condition on organization.tier:

TypeScript
// flags-multi.ts// docs-example: not typechecked here, because `identify` is a property of the// Flags SDK's flag() declaration, and this repo does not install flags.identify: ({ cookies }) => ({  kind: 'multi',  user: { kind: 'user', key: cookies.get('uid')?.value ?? 'anonymous' },  organization: { kind: 'organization', key: 'org_456', tier: 'enterprise' },})
TypeScript8 lines

Identity is worth writing

identify is where a decision gets its visitor. Without one, results are attributed to whoever the fallback finds. The fallback, in order:

  1. The avsb_anon_id cookie. Set it server-side with the withAvsb middleware from @avsbhq/next, which maintains it on every request. Do not count on the browser SDK for it: the browser persists the same id in localStorage first and only writes a cookie when localStorage is unavailable, so on most sites this cookie exists only because your middleware wrote it.
  2. The web snippet's _avsb_visitor cookie, decoded the way the snippet writes it. When Web Experiments is installed on the same domain, this is what makes the flag decision and the experiment exposure land on one visitor instead of two. A cookie of that name that does not parse is ignored.
  3. Nothing. The flag returns its defaultValue, no exposure is recorded, and one warning per flag key says what to add.

That last case is deliberate: inventing an id per request would split one visitor across every page view and quietly corrupt the experiment.

readSnippetVisitorId(rawCookieValue: string | null): string | null is exported if you want that decoding inside your own identify.

Exposures

Every decision records an exposure by default, because a Flags SDK read happens at request time for one visitor: that is a decision, and results need it.

Turn it off for flags you read early (in middleware, or in a layout) and show later:

TypeScript
// lib/avsbAdapter.tsimport { createAvsbAdapter } from '@avsbhq/flags-adapter'export const avsbAdapter = createAvsbAdapter({ exposure: 'off' })
TypeScript4 lines

Then record the exposure where the visitor actually sees the variation, either with useExposure from a framework SDK on the client, or manualExposure on a bound server client.

Bring your own client

An app that already runs AvsbServer should hand it over, so the process holds one datafile, one polling loop, and one event queue:

TypeScript
// lib/avsb.tsimport { AvsbServer } from '@avsbhq/node'import { createAvsbAdapter } from '@avsbhq/flags-adapter'export const avsb = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY ?? '' })export const adapter = createAvsbAdapter({ client: avsb })
TypeScript6 lines

AvsbServer satisfies the adapter's evaluator interface, so nothing needs casting. Pass a factory instead of an instance to defer construction until the first read.

When something is wrong

Nothing in this adapter throws. Each of these ends with the flag's declared default value and one log line:

What happenedWhat you see
No SDK keyOne error naming AVSB_SDK_KEY and where the key lives.
Datafile never loadedOne error with the HTTP status and the URL tried.
Nobody could be identifiedOne warning per flag key naming identify() and the cookie.
Flag key not in this environmentNothing logged. The default value serves silently; a raw client from avsbClient() shows flag.source as not_found.

What this adapter does not do

  • It does not evaluate in the browser. The Flags SDK runs flags on the server. For client-side reading use @avsbhq/react, @avsbhq/vue, @avsbhq/svelte, @avsbhq/solid or @avsbhq/angular.
  • It does not expose Flag<T> metadata (reasons, rule ids) through a flag read, because a flag value has to survive serialisation. Reach for avsbClient() when you need the whole decision.
  • It is not an OpenFeature provider. That is a separate integration.

What's next

Was this helpful?