Skip to main content

Hono (@ops-ai/toggly-hono)

Examples and requirements​

Open the Hono sample. Read the README, then src/catalog.js for flag inputs, src/app.js for request-scoped middleware and Order checks, and test/http.test.js for gate and isolation expectations. The catalog lists current sample availability.

Identity, groups, and claims are targeting inputs, not authentication. Populate them from your trusted session; a caller-controlled identity header does not authorize a user.

Requires Node.js 18+ and Hono 4.x (^4.0.0 in adapter 0.3.0). The compatibility suite covers current Hono 4.13.7 with the Node server adapter, packed ESM and CommonJS consumers, typechecking, request isolation, gates, refresh, signed-definition rejection, and shutdown. Node 18/20/22 keep unit coverage; Node 24 and 26 run the packed-host checks.

Install​

npm install @ops-ai/toggly-hono

Middleware​

import { Hono } from 'hono'
import { togglyMiddleware, featureGate, closeHonoToggly } from '@ops-ai/toggly-hono'

const app = new Hono()

app.use(
'*',
togglyMiddleware({
appKey: process.env.TOGGLY_APP_KEY!,
environment: 'Production',
verifySignatures: true,
enableStreaming: true,
}),
)

app.get('/dashboard', async (c) => {
const toggly = c.get('toggly')
if (await toggly.isFeatureOn('new-dashboard')) {
return c.json({ version: 'v2' })
}
return c.json({ version: 'v1' })
})

app.get('/beta', featureGate({ featureKey: 'beta-access' }), (c) =>
c.json({ ok: true }),
)

process.on('SIGTERM', () => closeHonoToggly())

Helpers live on the Hono context via c.get('toggly').

Configure identity, groups, and claims once on middleware. Segment request.* fields are filled from headers via fromHttpRequest. Then toggly.isFeatureOn('X') needs no context argument.

app.use(
'*',
togglyMiddleware({
appKey: process.env.TOGGLY_APP_KEY!,
getIdentity: (c) => c.get('user')?.id,
getGroups: (c) => c.get('user')?.roles ?? [],
getClaims: (c) => ({ role: c.get('user')?.role ?? '' }),
// Or getContext: (c) => ({ identity, groups, claims, request })
}),
)

app.get('/checkout', async (c) => {
const toggly = c.get('toggly')
if (await toggly.isFeatureOn('MobileCheckout')) {
return c.json({ flow: 'mobile' })
}
return c.json({ flow: 'desktop' })
})

Default identity (when getIdentity is unset): x-toggly-identity header.

When you supply getContext, missing request keys are still filled from headers. Prefer getIdentity / getGroups / getClaims for the happy path.

Overrides and entity checks​

In the adapter, the request helper isFeatureOn(key) accepts only the flag key and uses the middleware's context. Passing a second argument does not override that context. Call its core client explicitly and merge the fields you want to preserve:

const toggly = c.get('toggly')
const evaluation = {
...toggly.context,
identity: 'impersonated-user',
claims: { ...toggly.context.claims, role: 'admin' },
}
await toggly.client.isFeatureOn('MobileCheckout', evaluation)

// A ready entity context keeps Order data separate from user targeting.
await toggly.client.isFeatureOn('OrderBadge', toggly.context, {
kind: 'Order',
key: 'ORD-42',
attributes: { Status: 'Paid' },
})

The entity kind/property must match the feature definition. See Node entity context for mapper registration and HTTP segment filters for request fields.

Feature gates​

Same gate options as Express. Also: featureRoutes, withFeature, featuresHandler, getHonoToggly.

Core config: Configuration.

Feature snapshot endpoint​

featuresHandler is the Hono handler itself; pass it without calling it. Register it after the middleware:

app.get('/features', featuresHandler)

The response pairs the shared client's boolean feature snapshot with the current request's identity. It does not re-evaluate that snapshot using the request's groups, claims, or Order entity. An identity in the response therefore does not mean the returned flags were evaluated for that user. Use the request's isFeatureOn(key) helper for request-targeted decisions, or its client with explicit context/entity arguments as shown above.