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
npm install @avsbhq/flags-adapter flagsflags 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
// 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', }),})// app/page.tsximport { newCheckout } from '../flags'export default async function Page() { const showNewCheckout = await newCheckout() return showNewCheckout ? <NewCheckout /> : <Checkout />}Add your SDK key
AVSB_SDK_KEY=sdk_production_ttqm0eaj4vth1krcb2xnThat environment variable is the only configuration avsbAdapter needs.
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
// 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}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:
// 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' },})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:
- The
avsb_anon_idcookie. Set it server-side with thewithAvsbmiddleware from@avsbhq/next, which maintains it on every request. Do not count on the browser SDK for it: the browser persists the same id inlocalStoragefirst and only writes a cookie whenlocalStorageis unavailable, so on most sites this cookie exists only because your middleware wrote it. - The web snippet's
_avsb_visitorcookie, 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. - 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:
// lib/avsbAdapter.tsimport { createAvsbAdapter } from '@avsbhq/flags-adapter'export const avsbAdapter = createAvsbAdapter({ exposure: 'off' })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:
// 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 })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 happened | What you see |
|---|---|
| No SDK key | One error naming AVSB_SDK_KEY and where the key lives. |
| Datafile never loaded | One error with the HTTP status and the URL tried. |
| Nobody could be identified | One warning per flag key naming identify() and the cookie. |
| Flag key not in this environment | Nothing 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/solidor@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 foravsbClient()when you need the whole decision. - It is not an OpenFeature provider. That is a separate integration.
What's next
- Next.js (App Router): the middleware that maintains
avsb_anon_id. - OpenFeature: the other vendor-neutral flag API A vs B works behind.
- SDK installation:
Flag<T>, evaluation sources, and the shared options.