Featurevisor

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

Command
npm install flags @featurevisor/sdk

Keys

The Flags SDK has one kind of key, so the adapter uses the same convention as the OpenFeature providers:

ReadsKeyExample
A flag<featureKey>checkout
A variation<featureKey>:variationpricing:variation
A feature variable<featureKey>:<variableKey>buyButton:label
A global variablevariable:<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:

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 context
export 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.
  • refreshInterval suits 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:

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:

src/app/page.tsx
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:

src/pages/index.tsx
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 />
}
Previous
React Native
Next
Vue.js