/Docs

Hono integration

This guide assumes a Hono application running on Node.js 18+, Cloudflare Workers, or Bun. By the end you'll have per-request flag evaluation available on every route via the Hono context variable, using @avsbhq/node and the @avsbhq/utils/middleware/hono adapter.

1

Install

bash
1npm install @avsbhq/node@^1 @avsbhq/utils@^1
Info
Hono runs across multiple runtimes. On Cloudflare Workers, consider using @avsbhq/edge/cloudflare instead for a lighter-weight client that uses KV for datafile caching.
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] degraded init', result.error)
9 }
10}
4

Mount the middleware

The honoMiddleware from @avsbhq/utils/middleware/hono stores the per-request UserBoundClient in the Hono context variable c.var.avsb.

src/app.ts
ts
1import { Hono } from 'hono'
2import { honoMiddleware } from '@avsbhq/utils/middleware/hono'
3import { avsb } from './avsb'
4
5const app = new Hono()
6
7// Mount before route handlers.
8app.use(
9 '*',
10 honoMiddleware(avsb, {
11 contextFrom: (c) => {
12 const uid = c.req.header('x-user-id')
13 if (!uid) return undefined
14 return { kind: 'user', key: uid }
15 },
16 withDecisionLog: true,
17 })
18)
19
20export default app
Info
honoMiddleware is also available from @avsbhq/node as a single-package alternative.
5

Read a flag in a route handler

src/routes/checkout.ts
ts
1import { Hono } from 'hono'
2
3const checkout = new Hono()
4
5checkout.post('/session', (c) => {
6 const checkoutV2 = c.var.avsb.getBoolFlag('checkout-v2', false)
7
8 return c.json({
9 flow: checkoutV2.value ? 'v2' : 'legacy',
10 variationKey: checkoutV2.variationKey ?? null,
11 })
12})
13
14export default checkout
6

Track an event

ts
1checkout.post('/complete', async (c) => {
2 const { amount } = await c.req.json<{ amount: number }>()
3
4 c.var.avsb.track('purchase', {
5 value: amount,
6 properties: { runtime: 'hono' },
7 })
8
9 return c.json({ success: true })
10})
7

Use AsyncLocalStorage for implicit context

Service functions that do not receive the Hono context can still access the current user's client via getRequestClient:

src/services/pricingService.ts
ts
1import { getRequestClient } from '@avsbhq/utils'
2
3export function getDynamicPrice(basePrice: number): number {
4 const client = getRequestClient()
5 const pricingFlag = client.getStringFlag('dynamic-pricing', 'standard')
6
7 return pricingFlag.value === 'surge' ? basePrice * 1.2 : basePrice
8}

Graceful shutdown

For Node.js deployments, handle SIGTERM to flush events before exit:

ts
1import { serve } from '@hono/node-server'
2import { avsb, waitForAvsb } from './avsb'
3import app from './app'
4
5await waitForAvsb()
6
7const server = serve({ fetch: app.fetch, port: 3000 })
8
9process.on('SIGTERM', async () => {
10 server.close(async () => {
11 await avsb.close()
12 process.exit(0)
13 })
14})

On Cloudflare Workers, use ctx.waitUntil(avsb.flush()) at the end of each request handler to flush events without blocking the response.

Testing

ts
1import { createMockClient, TestData } from '@avsbhq/test'
2import { testClient } from 'hono/testing'
3import app from './app'
4
5const td = TestData.flag('checkout-v2').booleanFlag().fallthroughVariation(true)
6const mockAvsb = createMockClient({ flags: [td.build()] })
7
8// Replace the middleware's server reference for the test.
9const res = await testClient(app).checkout.session.$post(
10 {},
11 { headers: { 'x-user-id': 'u_test' } }
12)
13
14expect((await res.json()).flow).toBe('v2')

What's next