Fastify (@ops-ai/toggly-fastify)
Examples and requirements
Open the Fastify sample. For available samples, read the README, then src/catalog.js for flag definitions, src/app.js for request-bound evaluation and Order checks, and test/http.test.js for expected responses. The catalog is the source of 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.
The adapter declares Fastify 4.x or 5.x (^4.0.0 || ^5.0.0) and has a
Node.js 18+ package floor. Choose a Node runtime for the Fastify major you
run:
| Host | Required Node.js | Tested host |
|---|---|---|
| Fastify 4.x (retained) | Node 18+ | Fastify 4.29.1 on Node 18.20.8 |
| Fastify 5.x | Node 20+ | Fastify 5.12.4 on Node 20.19.6 |
These are Fastify host floors. Fastify 5's Node 20 requirement comes from Fastify itself; it does not raise the adapter's Node 18 package floor. The maintained sample uses Fastify 5 on Node 22.
Install
npm install @ops-ai/toggly-fastify
Plugin
import Fastify from 'fastify'
import { togglyPlugin, featureGate, closeFastifyToggly } from '@ops-ai/toggly-fastify'
const app = Fastify()
await app.register(togglyPlugin, {
appKey: process.env.TOGGLY_APP_KEY!,
environment: 'Production',
verifySignatures: true,
enableStreaming: true,
})
app.get('/dashboard', async (request, reply) => {
if (await request.toggly!.isFeatureOn('new-dashboard')) {
return { version: 'v2' }
}
return { version: 'v1' }
})
app.get(
'/beta',
{ preHandler: featureGate({ featureKey: 'beta-access' }) },
async () => ({ ok: true }),
)
// Plugin closes the client on Fastify onClose; optional explicit cleanup:
process.on('SIGTERM', () => closeFastifyToggly())
request.toggly mirrors Express: client, features, identity, context, and evaluation helpers.
Ambient EvalContext (recommended)
Configure identity, groups, and claims once on the plugin. Segment
request.* fields are filled from headers via fromHttpRequest. Then
request.toggly.isFeatureOn('X') needs no context argument.
await app.register(togglyPlugin, {
appKey: process.env.TOGGLY_APP_KEY!,
getIdentity: (request) => request.user?.id,
getGroups: (request) => request.user?.roles ?? [],
getClaims: (request) => ({ role: request.user?.role ?? '' }),
// Or getContext: (request) => ({ identity, groups, claims, request })
})
app.get('/checkout', async (request, reply) => {
if (await request.toggly!.isFeatureOn('MobileCheckout')) {
return { flow: 'mobile' }
}
return { 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 = request.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
Gate options include featureKey, requirement ('all' or 'any'), negate, onDisabled, redirectTo, and redirectStatus. All means every key must be on; any means at least one is on. Negation inverts that combined result. Without a custom disabled action, the gate sends a 404.
onDisabled(request) does not receive reply and returning from it does not stop Fastify's route handler. Do not use a returning callback as access control; use the default response or a separate preHandler that explicitly sends/throws. withFeature(key, options) returns a preHandler, not a wrapped route handler.
featuresHandler returns the client's state snapshot and request identity; it does not re-evaluate all flags against the request's targeting context. Use the request evaluation helpers for decisions. The adapter shares a process-level client; app.close() closes it. Do not use separate app instances to isolate tenants/configurations within one process.
Helpers: featureRoutes, withFeature, featuresHandler, getFastifyToggly.