Fastify
This guide is for a Fastify 4 or 5 server on Node.js 18 or later. A feature flag is a setting in your code you can turn on, off, or change without a new deploy. By the end, any route can read one from request.avsb. You get this by installing two packages, @avsbhq/node and the @avsbhq/utils/middleware/fastify plugin.
Install
Install @avsbhq/node and @avsbhq/utils.
Get your SDK key
Open Environments in your project's sidebar and copy the SDK key.
Set up the server SDK
Create one AvsbServer when your app starts. Wait for onReady() before you handle any requests.
Register the Fastify plugin
Register the plugin before any routes that need flags. It adds request.avsb to every request that comes in after it.
Read a flag in a route
Read flags from request.avsb inside any route that runs after the plugin.
Track an event
Call request.avsb.track to record a conversion for the current user.
Share a client without passing it around
Deep in your code, call getRequestClient to get the current user's client. You never have to pass request down through every function. Fastify has no built-in way to do this, so the plugin uses a Node.js feature called AsyncLocalStorage to make it work.
Install the packages:
npm install @avsbhq/node@^1 @avsbhq/utils@^1Copy the SDK key into your environment file (.env):
AVSB_SDK_KEY=sdk_production_xxxxxxxxxxxxxxxx- Environments lives in the sidebar on its own, not inside Settings.
- Click Reveal to see the full key, then Copy to copy it.
Set up the server SDK (src/avsb.ts):
import { AvsbServer } from '@avsbhq/node'export const avsb = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! })export async function waitForAvsb(): Promise<void> { const result = await avsb.onReady() if (!result.success && !result.degraded) { console.warn('[avsb] SDK init degraded: serving defaults', result.error) }}Register the Fastify plugin before any routes that need flags (src/server.ts):
import Fastify from 'fastify'import { fastifyPlugin } from '@avsbhq/utils/middleware/fastify'import type { FastifyPluginOptions } from '@avsbhq/utils/middleware/fastify'import { avsb, waitForAvsb } from './avsb'const fastify = Fastify({ logger: true })fastify.register(fastifyPlugin, { server: avsb, contextFrom: (request) => { // `contextFrom` receives the raw request, so narrow what you read off it. const headers = request.headers as Record<string, string | undefined> const uid = headers['x-user-id'] if (!uid) return undefined return { kind: 'user', key: uid } }, withDecisionLog: true,} satisfies FastifyPluginOptions)fastify.register(import('./routes/checkout'), { prefix: '/checkout' })async function start() { await waitForAvsb() await fastify.listen({ port: 3000 })}start()fastifyPlugin is also available from @avsbhq/node, so you can use just one package if you prefer.
Read a flag in a route (src/routes/checkout.ts):
import type { FastifyPluginAsync } from 'fastify'import type { UserBoundClient } from '@avsbhq/node'/** What the plugin decorates onto every request. */interface AvsbRequest { avsb: UserBoundClient body: unknown}interface RouteHost { post( path: string, handler: (request: AvsbRequest, reply: unknown) => Promise<unknown>, ): void}const checkoutRoutes: FastifyPluginAsync = async (fastify: RouteHost) => { fastify.post('/session', async (request, reply) => { const checkoutV2 = request.avsb.getBoolFlag('checkout_v2', false) return { flow: checkoutV2.value ? 'v2' : 'legacy', variationKey: checkoutV2.variationKey, } })}export default checkoutRoutesTrack an event:
fastify.post('/purchase', async (request: AvsbRequest, reply: unknown) => { const body = request.body as { amount: number } // `revenue` is money in major units; `value` is the separate // numeric-metric column. request.avsb.track('purchase', { revenue: body.amount }) return { success: true }})Call getRequestClient deep in your own code, for example in a service file (src/services/billingService.ts):
import { getRequestClient } from '@avsbhq/utils'import { applyBulkDiscount, applyStandardPricing } from './pricing'export async function applyDiscount(userId: string) { // Returns null outside a request scope, so handle that branch. const client = getRequestClient() if (!client) return applyStandardPricing(userId) const discountFlag = client.getStringFlag('discount_strategy', 'none') if (discountFlag.value === 'bulk') { return applyBulkDiscount(userId) } return applyStandardPricing(userId)}TypeScript type augmentation
Declare request.avsb once, in your own project (src/types/fastify.d.ts). You do not add any import for this. Your project owns the declaration, so it never clashes with another package's copy.
// docs-example: not typechecked here, because it augments the `fastify` module,// and this repo does not install fastify, so the module being augmented does not// exist for the standalone TypeScript program this documentation gate builds.import type { UserBoundClient } from '@avsbhq/node'declare module 'fastify' { interface FastifyRequest { avsb: UserBoundClient }}Graceful shutdown
Fastify has a built-in onClose hook. Use it to send any pending events before the server shuts down:
fastify.addHook('onClose', async () => { await avsb.close()})process.on('SIGTERM', async () => { await fastify.close()})Testing
Use @avsbhq/test to fake the server in your route tests, so a test never calls the real API.
import { createMockServer, flagsFromTestData, TestData } from '@avsbhq/test'import { build } from './server' // factory that returns the fastify instanceconst td = TestData.flag('checkout_v2').booleanFlag().fallthroughVariation(true)const mockClient = createMockServer(flagsFromTestData([td.build()]))// Override the plugin's server with a mock before running routes.const app = build({ avsbServer: mockClient })const res = await app.inject({ method: 'POST', url: '/checkout/session' })expect(JSON.parse(res.body).flow).toBe('v2')What's next
- Multi-context identity
- Decision logging
@avsbhq/nodeon npm: the package README, with the fullAvsbServerAPI.