Fastify integration
This guide assumes a Fastify 4 or 5 server running on Node.js 18+. By the end you'll have per-request flag evaluation available on every route via request.avsb, using @avsbhq/node and the @avsbhq/utils/middleware/fastify plugin adapter.
Install
1npm install @avsbhq/node@^1 @avsbhq/utils@^1Obtain your SDK key
Open Settings > Environments in your A vs B project and copy the server-side SDK key:
1AVSB_SDK_KEY=sdk_production_xxxxxxxxxxxxxxxxInitialise the server SDK
1import { AvsbServer } from '@avsbhq/node'2
3export const avsb = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! })4
5export async function waitForAvsb(): Promise<void> {6 const result = await avsb.onReady()7 if (!result.success && !result.degraded) {8 console.warn('[avsb] SDK init degraded — serving defaults', result.error)9 }10}Register the Fastify plugin
Register the plugin before any routes that need flag evaluation. The plugin decorates every incoming request with request.avsb.
1import Fastify from 'fastify'2import { fastifyPlugin } from '@avsbhq/utils/middleware/fastify'3import { avsb, waitForAvsb } from './avsb'4
5const fastify = Fastify({ logger: true })6
7fastify.register(fastifyPlugin, {8 server: avsb,9 contextFrom: (request) => {10 const uid = request.headers['x-user-id'] as string | undefined11 if (!uid) return undefined12 return { kind: 'user', key: uid }13 },14 withDecisionLog: true,15})16
17fastify.register(import('./routes/checkout'), { prefix: '/checkout' })18
19async function start() {20 await waitForAvsb()21 await fastify.listen({ port: 3000 })22}23
24start()fastifyPlugin is also re-exported from @avsbhq/node for projects that prefer a single dependency.Read a flag in a route handler
1import type { FastifyPluginAsync } from 'fastify'2
3const checkoutRoutes: FastifyPluginAsync = async (fastify) => {4 fastify.post('/session', async (request, reply) => {5 const checkoutV2 = request.avsb.getBoolFlag('checkout-v2', false)6
7 return {8 flow: checkoutV2.value ? 'v2' : 'legacy',9 variationKey: checkoutV2.variationKey,10 }11 })12}13
14export default checkoutRoutesTrack an event
1fastify.post('/purchase', async (request, reply) => {2 const body = request.body as { amount: number }3
4 request.avsb.track('purchase', {5 value: body.amount,6 properties: { route: '/purchase' },7 })8
9 return { success: true }10})Use AsyncLocalStorage for implicit context
Deep in your service layer, call getRequestClientto access the current user's client without threading request through function arguments:
1import { getRequestClient } from '@avsbhq/utils'2
3export async function applyDiscount(userId: string) {4 const client = getRequestClient()5 const discountFlag = client.getStringFlag('discount-strategy', 'none')6
7 if (discountFlag.value === 'bulk') {8 return applyBulkDiscount(userId)9 }10
11 return applyStandardPricing(userId)12}TypeScript type augmentation
Import the Fastify type augmentation from @avsbhq/node/fastify to get typed request.avsb across all route handlers:
1import '@avsbhq/node/fastify'2// FastifyRequest.avsb is now typed as UserBoundClientGraceful shutdown
Fastify has a built-in onClose hook. Register it to flush pending events before the server closes:
1fastify.addHook('onClose', async () => {2 await avsb.close()3})4
5process.on('SIGTERM', async () => {6 await fastify.close()7})Testing
1import { createMockClient, TestData } from '@avsbhq/test'2import { build } from './server' // factory that returns the fastify instance3
4const td = TestData.flag('checkout-v2').booleanFlag().fallthroughVariation(true)5const mockClient = createMockClient({ flags: [td.build()] })6
7// Override the plugin's server with a mock before running routes.8const app = build({ avsbServer: mockClient })9
10const res = await app.inject({ method: 'POST', url: '/checkout/session' })11expect(JSON.parse(res.body).flow).toBe('v2')