Express (@ops-ai/toggly-express)
Examples and requirements
Open the Express 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 Express 4 or 5 (express >=4.0.0). The
compatibility suite retains Express 4.22.2 and covers current Express
5.2.1 with 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-express
Middleware
import express from 'express'
import { togglyMiddleware, featureGate, closeExpressToggly } from '@ops-ai/toggly-express'
const app = express()
app.use(
togglyMiddleware({
appKey: process.env.TOGGLY_APP_KEY!,
environment: 'Production',
verifySignatures: true,
enableStreaming: true,
}),
)
app.get('/dashboard', async (req, res) => {
if (await req.toggly!.isFeatureOn('new-dashboard')) {
return res.json({ version: 'v2' })
}
res.json({ version: 'v1' })
})
process.on('SIGTERM', () => closeExpressToggly())
req.toggly exposes client, features, identity, context, isFeatureOn, isFeatureOff, and evaluateFeatureGate.
Ambient EvalContext (recommended)
Configure identity, groups, and claims once on middleware. HTTP segment
fields (request.userAgent / acceptLanguage / country) are filled from
headers via fromHttpRequest. Then req.toggly.isFeatureOn('X') needs no
context argument — same pattern as .NET AddTogglyWeb.
app.use(
togglyMiddleware({
appKey: process.env.TOGGLY_APP_KEY!,
getIdentity: (req) => req.user?.id,
getGroups: (req) => req.user?.roles ?? [],
getClaims: (req) => ({ role: req.user?.role ?? '', plan: req.user?.plan ?? '' }),
// Or getContext: (req) => ({ identity, groups, claims, request })
}),
)
app.get('/checkout', async (req, res) => {
// Ambient identity / groups / claims / request already bound
if (await req.toggly!.isFeatureOn('MobileCheckout')) {
return res.json({ flow: 'mobile' })
}
res.json({ flow: 'desktop' })
})
Default identity (when getIdentity is unset): x-toggly-identity header, then req.session.userId if present.
When you supply getContext, returned fields are used; missing request keys
are still filled from headers. Prefer getIdentity / getGroups / getClaims
unless you need a full custom context.
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 = req.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
app.get(
'/beta',
featureGate({ featureKey: 'beta-access' }),
(req, res) => res.json({ ok: true }),
)
app.get(
'/admin',
featureGate({
featureKey: ['admin', 'panel-v2'],
requirement: 'all',
redirectTo: '/upgrade',
}),
(req, res) => res.json({ admin: true }),
)
Disabled by default → 404 JSON. Override with onDisabled, redirectTo, or negate: true.
Also available: featureRoutes([...]), withFeature(key, handler, options?), featuresHandler(), getExpressToggly().
Core options (verifySignatures, cache, streaming) are documented under Configuration.
Feature snapshot endpoint
featuresHandler() is a factory returning an Express handler. 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.