Koa integration
This guide assumes a Koa 2 server running on Node.js 18+. By the end you'll have per-request flag evaluation available via ctx.state.avsb on every route, using @avsbhq/node and the @avsbhq/utils/middleware/koa adapter.
1
Install
bash
1npm install @avsbhq/node@^1 @avsbhq/utils@^12
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_xxxxxxxxxxxxxxxx3
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] degraded init', result.error)9 }10}4
Mount the middleware
The koaMiddleware from @avsbhq/utils/middleware/koa opens an AsyncLocalStorage scope and stores the UserBoundClient on ctx.state.avsb.
src/app.ts
ts
1import Koa from 'koa'2import Router from '@koa/router'3import bodyParser from '@koa/bodyparser'4import { koaMiddleware } from '@avsbhq/utils/middleware/koa'5import { avsb } from './avsb'6
7export const app = new Koa()8const router = new Router()9
10app.use(bodyParser())11
12// Mount before routes.13app.use(14 koaMiddleware(avsb, {15 contextFrom: (ctx) => {16 const uid = ctx.headers['x-user-id'] as string | undefined17 if (!uid) return undefined18 return { kind: 'user', key: uid }19 },20 withDecisionLog: true,21 })22)23
24// Routes defined below the middleware have access to ctx.state.avsb.25router.post('/checkout/session', (ctx) => {26 const checkoutV2 = ctx.state.avsb.getBoolFlag('checkout-v2', false)27 ctx.body = {28 flow: checkoutV2.value ? 'v2' : 'legacy',29 variationKey: checkoutV2.variationKey ?? null,30 }31})32
33app.use(router.routes())Info
koaMiddleware is also re-exported from @avsbhq/node for projects that prefer a single dependency.5
Track an event
ts
1router.post('/purchase', async (ctx) => {2 const { amount } = ctx.request.body as { amount: number }3
4 ctx.state.avsb.track('purchase', {5 value: amount,6 properties: { router: 'koa' },7 })8
9 ctx.body = { success: true }10})6
Use AsyncLocalStorage for implicit context
Service functions that run inside the request lifecycle can call getRequestClient without needing access to ctx:
src/services/inventoryService.ts
ts
1import { getRequestClient } from '@avsbhq/utils'2
3export function shouldShowLowStockBanner(): boolean {4 const client = getRequestClient()5 const lowStockFlag = client.getBoolFlag('low-stock-banner', false)6 return lowStockFlag.value7}7
Identify a user
Override the context mid-request by calling avsb.forUser(ctx) with a more specific context, then storing it on ctx.state for downstream handlers:
ts
1router.post('/login', async (ctx) => {2 const { userId, plan } = ctx.request.body as { userId: string; plan: string }3
4 // Replace the anonymous context with the authenticated user.5 ctx.state.avsb = avsb.forUser({ kind: 'user', key: userId, plan })6
7 ctx.body = { success: true }8})Graceful shutdown
ts
1import { app } from './app'2import { avsb, waitForAvsb } from './avsb'3
4await waitForAvsb()5const server = app.listen(3000)6
7process.on('SIGTERM', async () => {8 server.close(async () => {9 await avsb.close()10 process.exit(0)11 })12})Testing
ts
1import { createMockClient, TestData } from '@avsbhq/test'2import supertest from 'supertest'3import { app } from './app'4
5const td = TestData.flag('checkout-v2').booleanFlag().fallthroughVariation(true)6const mockAvsb = createMockClient({ flags: [td.build()] })7
8// In test setup, replace the real server reference used by the middleware.9const res = await supertest(app.callback())10 .post('/checkout/session')11 .set('x-user-id', 'u_test')12
13expect(res.body.flow).toBe('v2')