Hono
This guide is for a Hono app on Node.js 18+, Cloudflare Workers, or Bun. 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 c.var.avsb. You get this by installing two packages, @avsbhq/node and the @avsbhq/utils/middleware/hono adapter.
Install
Install @avsbhq/node and @avsbhq/utils. On Cloudflare Workers, @avsbhq/edge/cloudflare is a lighter option: it caches the datafile (the JSON file listing your project's flags) in KV instead of memory.
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.
Mount the middleware
Register honoMiddleware before your routes. For any request where it can identify the visitor, it adds a ready-to-use client at c.var.avsb.
Read a flag in a route handler
Read flags from c.var.avsb inside any route that runs after the middleware.
Track an event
Call c.var.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, without passing c down through every function. This uses a Node.js feature called AsyncLocalStorage, so it only works on Node.js, not on Workers or Bun.
Install the packages:
npm install @avsbhq/node@^1 @avsbhq/utils@^1Hono runs on several JavaScript runtimes. On Cloudflare Workers, @avsbhq/edge/cloudflare is a lighter-weight client built for that runtime, using KV for datafile caching instead of an in-memory poll.
Copy the SDK key into your environment file (.env):
AVSB_SDK_KEY=sdk_production_xxxxxxxxxxxxxxxxSet 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 failed: serving defaults', result.error) }}Mount the middleware before route handlers (src/app.ts):
import { Hono } from 'hono'import { honoMiddleware } from '@avsbhq/utils/middleware/hono'import { avsb } from './avsb'const app = new Hono()// Mount before route handlers.app.use( '*', honoMiddleware(avsb, { contextFrom: (c) => { // `contextFrom` receives the raw request, so it reads the same way on // every runtime Hono supports. Return undefined to skip AvsB for a // request: a health check, an asset route, anything with no visitor. const uid = c.req.raw.headers.get('x-user-id') if (!uid) return undefined return { kind: 'user', key: uid } }, withDecisionLog: true, }))export default apphonoMiddleware lives only in @avsbhq/utils/middleware/hono. The Node SDK re-exports Express and Fastify for convenience, but not Hono, Koa, or the other framework adapters. @avsbhq/utils is the one place to import honoMiddleware from.
When contextFrom returns undefined, the middleware calls next() immediately and never sets c.var.avsb at all. It is not a client with no context: the value is simply missing. A route handler that skips the check and calls c.var.avsb.getBoolFlag(...) throws Cannot read properties of undefined. Guard for it: if (!c.var.avsb) return c.text('', 204), or default the flow when contextFrom has nothing to key on (no session cookie, a health check route, and so on).
Read a flag in a route handler (src/routes/checkout.ts):
import { Hono } from 'hono'import type { RequestBoundClient } from '@avsbhq/utils'// Tell Hono's type system what the middleware put on the context, so// `c.var.avsb` is typed everywhere in your app.declare module 'hono' { interface ContextVariableMap { avsb: RequestBoundClient }}const checkout = new Hono().post('/session', (c) => { const checkoutV2 = c.var.avsb.getBoolFlag('checkout_v2', false) return c.json({ flow: checkoutV2.value ? 'v2' : 'legacy', variationKey: checkoutV2.variationKey ?? null, })})export default checkoutTrack an event:
checkout.post('/complete', async (c) => { const { amount } = await c.req.json<{ amount: number }>() // `revenue` is money in major units; `value` is the separate // numeric-metric column. c.var.avsb.track('purchase', { revenue: amount }) return c.json({ success: true })})Share a client without passing it around: service functions that never see c can still read the current user's client, using getRequestClient (src/services/pricingService.ts):
import { getRequestClient } from '@avsbhq/utils'export function getDynamicPrice(basePrice: number): number { // Returns null outside a request scope, and on edge runtimes with no // AsyncLocalStorage (Cloudflare Workers, Bun's edge mode), so handle that // branch. const client = getRequestClient() if (!client) return basePrice const pricingFlag = client.getStringFlag('dynamic_pricing', 'standard') return pricingFlag.value === 'surge' ? basePrice * 1.2 : basePrice}Graceful shutdown
For Node.js deployments, handle SIGTERM to flush events before exit:
import { serve } from '@hono/node-server'import { avsb, waitForAvsb } from './avsb'import app from './app'await waitForAvsb()const server = serve({ fetch: app.fetch, port: 3000 })process.on('SIGTERM', () => { server.close(async () => { await avsb.close() process.exit(0) })})On Cloudflare Workers, call ctx.waitUntil(avsb.flush()) at the end of each request handler. It flushes queued events without blocking the response.
Testing
Build a small test app with a mock server wired directly into the middleware. Swapping the real avsb singleton for a mock only works if the mock is the one actually passed to honoMiddleware, not created and left unused:
import { Hono } from 'hono'import { testClient } from 'hono/testing'import { createMockServer, flagsFromTestData, TestData } from '@avsbhq/test'import { honoMiddleware } from '@avsbhq/utils/middleware/hono'const td = TestData.flag('checkout_v2').booleanFlag().fallthroughVariation(true)const mockAvsb = createMockServer(flagsFromTestData([td.build()]))// Wire the mock into the middleware itself, the same way `src/app.ts` wires// the real `avsb` singleton.const app = new Hono() .use('*', honoMiddleware(mockAvsb, { contextFrom: () => ({ kind: 'user', key: 'test-user' }) })) .post('/checkout/session', (c) => { const checkoutV2 = c.var.avsb.getBoolFlag('checkout_v2', false) return c.json({ flow: checkoutV2.value ? 'v2' : 'legacy' }) })const res = await testClient(app).checkout.session.$post()expect((await res.json()).flow).toBe('v2')What's next
- Cloudflare Workers integration: lighter-weight edge client for Workers deployments.
- Multi-context identity