Skip to main content

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:

HostRequired Node.jsTested host
Fastify 4.x (retained)Node 18+Fastify 4.29.1 on Node 18.20.8
Fastify 5.xNode 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.

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.