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.

1

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.

2

Get your SDK key

Open Environments in your project's sidebar and copy the SDK key.

3

Set up the server SDK

Create one AvsbServer when your app starts. Wait for onReady() before you handle any requests.

4

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.

5

Read a flag in a route handler

Read flags from c.var.avsb inside any route that runs after the middleware.

6

Track an event

Call c.var.avsb.track to record a conversion for the current user.

7

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:

Shell
npm install @avsbhq/node@^1 @avsbhq/utils@^1
Shell1 line
Info

Hono 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):

Shell
AVSB_SDK_KEY=sdk_production_xxxxxxxxxxxxxxxx
Shell1 line

Set up the server SDK (src/avsb.ts):

TypeScript
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)  }}
TypeScript10 lines

Mount the middleware before route handlers (src/app.ts):

TypeScript
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 app
TypeScript23 lines
Info

honoMiddleware 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.

The one mistake: c.var.avsb is not always there

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):

TypeScript
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 checkout
TypeScript21 lines

Track an event:

TypeScript
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 })})
TypeScript9 lines

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):

TypeScript
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}
TypeScript13 lines

Graceful shutdown

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

TypeScript
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)  })})
TypeScript14 lines

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:

TypeScript
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')
TypeScript19 lines

What's next

Was this helpful?