Framework guides
Fastify
A Fastify process keeps one Featurevisor instance for every request, and each request evaluates with its own context.
Install#
In an existing Fastify application:
npm install @featurevisor/sdkSave the refresh helper from the Node.js guide as datafile-refresh.mjs, next to server.mjs. You need only the helper, not the Node.js startup code.
The server#
This server loads the datafile before it listens, refreshes it every five minutes, and closes cleanly on SIGTERM. It expects an authentication hook, registered before the routes in the same scope, that sets request.user from a verified session:
import Fastify from 'fastify'import { createFeaturevisor } from '@featurevisor/sdk'import { refreshDatafile, waitForRefresh } from './datafile-refresh.mjs'const DATAFILE_URL = 'https://cdn.example.com/featurevisor-all.json'const fastify = Fastify({ logger: true })const f = createFeaturevisor({})// Register your authentication hook here, before the routesfastify.get('/checkout', async (request, reply) => { if (!request.user) return reply.code(401).send() const context = { userId: String(request.user.id), country: request.user.country, } return f.isEnabled('checkout', context) ? 'New checkout' : 'Checkout'})let refreshTimerfastify.addHook('onClose', async () => { clearInterval(refreshTimer) await waitForRefresh(f) await f.close()})const start = async () => { await refreshDatafile(f, DATAFILE_URL) await fastify.listen({ port: 3000 }) refreshTimer = setInterval(() => { refreshDatafile(f, DATAFILE_URL).catch((error) => fastify.log.error(error), ) }, 5 * 60 * 1000) refreshTimer.unref() const shutdown = () => { clearInterval(refreshTimer) fastify.close().catch(reportError) } process.once('SIGTERM', shutdown) process.once('SIGINT', shutdown)}function reportError(error) { fastify.log.error(error) process.exitCode = 1}start() .catch(async (error) => { reportError(error) await fastify.close() }) .catch(reportError)Run it with node server.mjs. It needs a Node.js version with fetch and AbortSignal.timeout.
- If the first datafile cannot be loaded, the server does not start.
- A failed refresh keeps the previous datafile.
- On shutdown, Fastify lets the current requests finish, then
onClosewaits for any refresh in progress and closes the instance. - If your application already handles signals, add these steps to its handler instead. Never add a second refresh loop, and never call
setContext()on the shared instance for one user.
A child instance per request#
When routes in several modules evaluate for the same user, give each request a child instance with that user's context. Fastify's request decorators cannot start with an object, so declare the property as null and set it in a hook after authentication:
fastify.decorateRequest('features', null)fastify.addHook('preHandler', async (request) => { if (request.user) { request.features = f.spawn({ userId: String(request.user.id), country: request.user.country, }) }})async function closeFeatures(request) { request.features?.close() request.features = null}fastify.addHook('onResponse', closeFeatures)fastify.addHook('onRequestAbort', closeFeatures)A route then reads values without building the context again:
fastify.get('/basket', async (request, reply) => { if (!request.features) return reply.code(401).send() return { newCheckout: request.features.isEnabled('checkout'), buyLabel: request.features.getVariable('buyButton', 'label'), }})For user-4 in the Netherlands, this returns newCheckout: true and buyLabel: 'Nu kopen'. The child follows the parent's datafile updates. onResponse does not run when the client aborts, so onRequestAbort releases the child in that case.
TypeScript#
Declare the property on FastifyRequest, in a custom.d.ts listed in tsconfig.json:
import type { FeaturevisorChildInstance } from '@featurevisor/sdk'import 'fastify'declare module 'fastify' { interface FastifyRequest { features: FeaturevisorChildInstance | null }}Your authentication library declares user the same way.

