Vercel Functions

Vercel offers two function runtimes, and A vs B has a package for each. Use @avsbhq/node in Serverless Functions, where a warm process can hold the datafile, stream updates, and keep sticky assignments. Use @avsbhq/edge in Edge Functions, where the isolate is short-lived and the datafile is cached in Vercel Edge Config.

The two SDKs deliberately do not have the same shape. The server SDK is stateless and takes a context per request. The edge client is bound to one request by its adapter, so it has no forUser and no identify at all.

Node.js runtime

1

Install

Add the Node SDK to your project.

2

Obtain your SDK key

Go to Environments in your A vs B project's sidebar and copy the SDK key for the environment this deployment serves. Add it to Vercel as an environment variable named AVSB_SDK_KEY. It does not need a client-side prefix: your functions do the evaluating.

3

Bootstrap the server SDK

Construct AvsbServer once at module scope so warm invocations reuse the datafile it already holds, and await onReady() before you read anything.

4

Bind the request context

server.forUser(context) returns a UserBoundClient with its own decision log. Reads on it need no further context.

5

Read a flag

Call a typed getter for the value plus the metadata that says where it came from.

6

Track an event

revenue carries money, value carries a quantity. They are two separate columns end to end.

Install the package:

Shell
npm install @avsbhq/node
Shell1 line

Add the key to your Vercel project's environment variables:

Shell
AVSB_SDK_KEY=sdk_production_ttqm0eaj4vth1krcb2xn
Shell1 line
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.

Bootstrap the server SDK at module scope:

TypeScript
import { AvsbServer } from '@avsbhq/node'import type { VercelRequest, VercelResponse } from '@vercel/node'const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY ?? '' })export default async function handler(req: VercelRequest, res: VercelResponse): Promise<void> {  const init = await server.onReady()  if (!init.success) {    // Flags serve the defaults you pass until a datafile arrives, and polling    // continues in the background. Survivable, not fatal.    console.warn('avsb init failed', init.source)  }  const userId = (req.headers['x-user-id'] as string | undefined) ?? 'anon'  const avsb = server.forUser({ kind: 'user', key: userId })  res.json({ showNewCheckout: avsb.getBoolFlag('checkout_v2', false).isEnabled() })}
TypeScript18 lines

onReady() never rejects and returns the same promise on repeated calls, so awaiting it at the top of every invocation costs nothing after the first.

Reading flags

TypeScript
import type { UserBoundClient, Flag } from '@avsbhq/node'interface CheckoutDecision {  enabled: boolean  variationKey: string | null  reasons: string[]}function decideCheckout(avsb: UserBoundClient): CheckoutDecision {  const flag: Flag<boolean> = avsb.getBoolFlag('checkout_v2', false)  return { enabled: flag.isEnabled(), variationKey: flag.variationKey, reasons: flag.reasons }}
TypeScript12 lines

flag.source is one of datafileOverride, runtimeOverride, sticky, rule, holdout, bandit, default, disabled, not_found, or not_ready. isEnabled() is true only when a real decision produced a truthy value, so it is always false for default, disabled, not_found, and not_ready. exists() answers "is this key in the datafile", and is false for both not_found and not_ready.

A typed getter checks the value against the type the dashboard declares for the flag. On a mismatch it logs a warning naming both types and returns your default; it never throws and never coerces.

Tracking events

TypeScript
import type { UserBoundClient } from '@avsbhq/node'function recordConversions(avsb: UserBoundClient): void {  avsb.track('purchase_completed', { revenue: 199.0 }) // money, decimal major units  avsb.track('items_added', { value: 3 }) // a quantity  avsb.track('signup_completed') // a count}
TypeScript7 lines

revenue and value are two separate columns, so one conversion can carry money, a quantity, or both. properties is accepted by the shared type but is not stored for conversions: the ingestion contract for a metric event keeps the event name, the two numbers, the timestamp, and the visitor id. The SDK warns once and drops them rather than pretending. Exposure events do carry properties.

track is synchronous. Events tracked before the datafile arrives are queued and sent once it does.

Multi-context identity

TypeScript
import type { AvsbServer, UserBoundClient } from '@avsbhq/node'function bindAccount(server: AvsbServer, userId: string, orgId: string): UserBoundClient {  return server.forUser({    kind: 'multi',    user: { kind: 'user', key: userId, plan: 'pro' },    organization: { kind: 'organization', key: orgId, tier: 'enterprise' },  })}
TypeScript9 lines

A rule can target organization.tier while bucketing on user.key.

Edge runtime

Vercel Edge Functions run in a V8 isolate: no Node built-ins, no persistent background timers. @avsbhq/edge is built for exactly that, and its Vercel adapter caches the datafile in Vercel Edge Config (Vercel's docs now call it Global Config) so evaluation needs no outbound fetch on a warm isolate.

1

Install

Add the edge client and the Edge Config helper.

2

Configure Edge Config

Create an Edge Config store and add its connection string to the project as EDGE_CONFIG.

3

Bootstrap with createVercelHandler

The factory builds one client per request, binds the visitor context, awaits init, and flushes correctly for the runtime.

4

Read a flag

The client inside your handler is already bound, so reads take no context argument.

5

Let the adapter flush

Pass the FetchEvent through and the flush runs on waitUntil; without one it is awaited before the response returns.

Install the packages:

Shell
npm install @avsbhq/edge @vercel/edge-config
Shell1 line

Bootstrap the edge client:

TypeScript
import { createVercelHandler } from '@avsbhq/edge/vercel'import { createClient } from '@vercel/edge-config'export const runtime = 'edge'const avsb = createVercelHandler({  sdkKey: process.env.AVSB_SDK_KEY ?? '',  edgeConfig: createClient(process.env.EDGE_CONFIG),  handler: async (req, client) => {    const banner = client.getBoolFlag('new_banner', false)    return Response.json({ enabled: banner.isEnabled() })  },})export function GET(req: Request): Promise<Response> {  return avsb(req)}
TypeScript17 lines

There is no onReady() to await and no forUser() to call. init() is awaited by the adapter before your handler runs, and the context the adapter built from the request is already bound to the client.

Edge Config is read-only at runtime

The adapter reads the key avsb_df_<sdkKey> from Edge Config, and after a network fetch it can only warm its own in-process map, because Edge Config writes go through the Vercel API out of band. If you want a cache shared across isolates, populate that key from CI or a cron. Without it, each cold isolate fetches the datafile once from the A vs B CDN and reuses it for the life of the isolate.

Freshness and retention are two different clocks

cacheTtlMs (default 5 minutes) decides how long a cached datafile counts as fresh; past it the client revalidates and, if that fails, serves the stale copy with degraded: true. memoryRetentionMs (default 1 hour) decides how long the in-process copy is kept at all.

Reading flags at the edge

TypeScript
import type { AvsbEdgeClient, EvaluationSource, Flag } from '@avsbhq/edge'function decideBanner(client: AvsbEdgeClient): { copy: string; source: EvaluationSource } {  const copy: Flag<string> = client.getStringFlag('banner_copy', 'Welcome back')  return { copy: copy.value, source: copy.source }}
TypeScript6 lines

Reading a flag before init() finishes returns your default with source: 'not_ready' and warns once per key, so a pre-ready read is never mistaken for a missing flag.

Tracking and flushing

TypeScript
import type { AvsbEdgeClient } from '@avsbhq/edge'function recordBannerClick(client: AvsbEdgeClient): void {  client.track('banner_clicked', { value: 1 })}
TypeScript5 lines

The adapter calls client.flushEvents() for you: on waitUntil when a FetchEvent is passed as the handler's second argument, and awaited before the response otherwise. Here, payload.value and payload.revenue land in their own columns, the same as the server. payload.properties is dropped here and kept only on exposure events.

Identity at the edge

The Vercel adapter resolves the visitor from the x-avsb-visitor-id header, then the A vs B snippet's _avsb_visitor cookie, and reads Vercel's x-vercel-ip-* geo headers for country, region, city, timezone, latitude, and longitude (the city arrives percent-encoded and is decoded for you). The client IP is never used as the bucketing key.

Pass contextFrom to replace that, building on the exported vercelContextFrom so you keep the platform data. There is no identify() at the edge: the context is fixed for the life of one request.

SSR hydration in Next.js on Vercel

If you are running a Next.js application, use the Next.js App Router integration instead of this page. It handles server-side evaluation and client hydration through the dedicated @avsbhq/next package.

Testing

createMockServer mirrors the server SDK, forUser included, and its bound client has the same read surface as the client an edge handler receives. One fixture drives both:

TypeScript
import { createMockServer, flagsFromTestData, TestData } from '@avsbhq/test'import { describe, it, expect } from 'vitest'const server = createMockServer(  flagsFromTestData([    TestData.flag('checkout_v2')      .booleanFlag()      .variationForUser('u_power', true)      .fallthroughVariation(false)      .build(),  ]),)describe('checkout decision', () => {  it('serves v2 to the power user', () => {    const avsb = server.forUser({ kind: 'user', key: 'u_power' })    expect(avsb.getBoolFlag('checkout_v2', false).value).toBe(true)  })  it('serves the fallthrough to everyone else', () => {    const avsb = server.forUser({ kind: 'user', key: 'u_other' })    expect(avsb.getBoolFlag('checkout_v2', false).value).toBe(false)  })  it('attributes a tracked event to the bound context', () => {    server.forUser({ kind: 'user', key: 'u_power' }).track('purchase', { revenue: 99 })    expect(server._tracked()).toEqual([      { eventKey: 'purchase', payload: { revenue: 99 }, contextKey: 'u_power' },    ])  })})
TypeScript31 lines

What's next

Was this helpful?