/Docs

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.

1

Install

bash
1npm install @avsbhq/node@^1 @avsbhq/utils@^1
2

Obtain your SDK key

Open Settings > Environments in your A vs B project and copy the server-side SDK key:

.env
bash
1AVSB_SDK_KEY=sdk_production_xxxxxxxxxxxxxxxx
3

Initialise the server SDK

src/avsb.ts
ts
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}
4

Register the Fastify plugin

Register the plugin before any routes that need flag evaluation. The plugin decorates every incoming request with request.avsb.

src/server.ts
ts
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 | undefined
11 if (!uid) return undefined
12 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()
Info
fastifyPlugin is also re-exported from @avsbhq/node for projects that prefer a single dependency.
5

Read a flag in a route handler

src/routes/checkout.ts
ts
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 checkoutRoutes
6

Track an event

ts
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})
7

Use AsyncLocalStorage for implicit context

Deep in your service layer, call getRequestClientto access the current user's client without threading request through function arguments:

src/services/billingService.ts
ts
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:

src/types/fastify.d.ts
ts
1import '@avsbhq/node/fastify'
2// FastifyRequest.avsb is now typed as UserBoundClient

Graceful shutdown

Fastify has a built-in onClose hook. Register it to flush pending events before the server closes:

ts
1fastify.addHook('onClose', async () => {
2 await avsb.close()
3})
4
5process.on('SIGTERM', async () => {
6 await fastify.close()
7})

Testing

ts
1import { createMockClient, TestData } from '@avsbhq/test'
2import { build } from './server' // factory that returns the fastify instance
3
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')

What's next