Framework guides
Next.js
In Next.js, decide flags on the server, where each request's user is known. Vercel's Flags SDK gives Next.js a flag API, and a short adapter connects that API to Featurevisor, in the App Router and the Pages Router alike.
You can also use the JavaScript SDK directly, as in Node.js. The adapter is for keeping one flag API in your application, whichever tool answers it.
Install#
npm install flags @featurevisor/sdkKeys#
The Flags SDK has one kind of key, so the adapter uses the same convention as the OpenFeature providers:
| Reads | Key | Example |
|---|---|---|
| A flag | <featureKey> | checkout |
| A variation | <featureKey>:variation | pricing:variation |
| A feature variable | <featureKey>:<variableKey> | buyButton:label |
| A global variable | variable:<variableKey> | variable:supportEmail |
Avoid features called variable and feature variables called variation, since those words select the other kinds.
The adapter#
Save the adapter as src/featurevisor.ts:
import type { Adapter } from 'flags'import { createFeaturevisor, type Featurevisor,} from '@featurevisor/sdk'export interface FeaturevisorAdapterOptions { datafileUrl: string refreshInterval?: number instance?: Featurevisor}// The attributes your flags pass as contextexport interface FeaturevisorEntitiesType { userId?: string country?: string}export function createFeaturevisorAdapter( options: FeaturevisorAdapterOptions,) { const f = options.instance ?? createFeaturevisor() let initialFetchCompleted = false let pendingFetch: Promise<void> | undefined function fetchAndSetDatafile() { if (pendingFetch) return pendingFetch pendingFetch = fetch(options.datafileUrl, { cache: 'no-store' }) .then((response) => { if (!response.ok) { throw new Error(`Request failed with status ${response.status}`) } return response.json() }) .then((datafile) => { if (!f.setDatafile(datafile, true)) { throw new Error('Datafile was not accepted') } initialFetchCompleted = true }) .catch((error) => { console.error('[Featurevisor] Could not fetch datafile:', error) if (!initialFetchCompleted) throw error }) .finally(() => { pendingFetch = undefined }) return pendingFetch } if (options.refreshInterval) { setInterval(async () => { // A failed first load still reaches the decision that awaits it await fetchAndSetDatafile().catch(() => {}) }, options.refreshInterval) } return function featurevisorAdapter< ValueType, EntitiesType extends FeaturevisorEntitiesType = FeaturevisorEntitiesType, >(): Adapter<ValueType, EntitiesType> { return { async decide({ key, entities }): Promise<ValueType> { if (!initialFetchCompleted) { await fetchAndSetDatafile() } const context = { userId: entities?.userId, country: entities?.country, } const [featureKey, variableKey] = key.split(':') if (featureKey === 'variable' && variableKey) { return f.getVariable(variableKey, context) as ValueType } if (variableKey === 'variation') { return f.getVariation(featureKey, context) as ValueType } if (variableKey) { return f.getVariable( featureKey, variableKey, context, ) as ValueType } return f.isEnabled(featureKey, context) as ValueType }, } }}- Create the adapter once per runtime, and refresh its instance from nowhere else.
- Decisions made while the datafile is loading wait for the same fetch.
- If the first load fails, the decision fails, so your application can handle missing configuration. After that, a failed refresh keeps the previous datafile.
refreshIntervalsuits a long running server. In serverless functions, fetch again when your cached copy is older than some limit instead.- Add a request timeout, and clear the timer if your runtime can stop the server.
Declare your flags#
Declare each value you read as a flag in src/flags.ts:
import { flag } from 'flags/next'import { createFeaturevisorAdapter } from './featurevisor'const featurevisorAdapter = createFeaturevisorAdapter({ datafileUrl: 'https://cdn.example.com/featurevisor-all.json', refreshInterval: 5 * 60 * 1000,})export const checkoutFlag = flag({ key: 'checkout', adapter: featurevisorAdapter<boolean>(),})export const pricingVariation = flag({ key: 'pricing:variation', adapter: featurevisorAdapter<string | null>(),})export const buyButtonLabel = flag({ key: 'buyButton:label', adapter: featurevisorAdapter<string>(),})export const supportEmail = flag({ key: 'variable:supportEmail', adapter: featurevisorAdapter<string>(),})Pass instance to the adapter to reuse an instance your application already has.
Flags need to know who is asking. Add an identify function that returns userId and country from the signed in session. Without one, every request is evaluated without a user. Never put a request's context on the shared instance.
App Router#
Call a flag in a server component, a route handler or middleware:
import { checkoutFlag } from '../flags'export default async function Page() { const newCheckout = await checkoutFlag() return newCheckout ? <NewCheckout /> : <Checkout />}Pages Router#
Call a flag in getServerSideProps, and pass its value to the page:
import type { GetServerSideProps, InferGetServerSidePropsType,} from 'next'import { checkoutFlag } from '../flags'export const getServerSideProps = (async ({ req }) => { const newCheckout = await checkoutFlag(req) return { props: { newCheckout } }}) satisfies GetServerSideProps<{ newCheckout: boolean }>export default function Page({ newCheckout,}: InferGetServerSidePropsType<typeof getServerSideProps>) { return newCheckout ? <NewCheckout /> : <Checkout />}Related#
- React for client components
- Node.js
- Example application

