Next.js (Pages Router)
@avsbhq/next ships a getServerSideProps wrapper for the Pages Router. It resolves the visitor, fetches the datafile, evaluates every flag, and hands _app.tsx one prop that is exactly the provider's props. The server-rendered HTML and the first client render agree, so there is no flash of default values on a page that uses it.
This page assumes Next.js 15 or later and React 18 or later. On the App Router, read Next.js (App Router) instead.
Install
One package. @avsbhq/core, @avsbhq/browser, @avsbhq/react and @avsbhq/utils arrive with it.
Add your SDK key
Open Environments in your A vs B project sidebar and copy the key for the environment you are wiring up.
Wrap getServerSideProps
getServerSideAvsb does the fetch, the anonymous visitor cookie, and the evaluation, then adds avsbBootstrap to your props.
Spread avsbBootstrap into the provider
In pages/_app.tsx. It carries the key, the context, the datafile, and the evaluated values.
Read flags and record exposures
Hooks come from @avsbhq/next. Reads never fire exposures; useExposure does.
Install
npm install @avsbhq/nextAdd your SDK key
# .env.localAVSB_SDK_KEY=sdk_production_ttqm0eaj4vth1krcb2xnOne server-side variable is enough. getServerSideAvsb reads it on the server and passes the key to the browser inside avsbBootstrap, so you do not need a NEXT_PUBLIC_ copy.
Wrap getServerSideProps
// pages/checkout.tsximport { getServerSideAvsb } from '@avsbhq/next/pages'export const getServerSideProps = getServerSideAvsb({ sdkKey: process.env.AVSB_SDK_KEY ?? '',})export default function CheckoutPage() { return <CheckoutContent />}The options, in full:
import type { GsspContext } from '@avsbhq/next/pages'import type { AnonCookieOptions } from '@avsbhq/next/server'import type { EvalContext } from '@avsbhq/next'interface GetServerSideAvsbOptions { /** The environment SDK key. */ sdkKey: string /** CDN base URL. Default 'https://cdn.avsb.cloud'. */ cdnHost?: string /** Maps the request to an evaluation context. Omit it for the anonymous cookie. */ contextFrom?: (ctx: GsspContext) => EvalContext /** Anonymous cookie name, lifetime, and attributes, when `contextFrom` is omitted. */ cookie?: AnonCookieOptions}GsspContext is the slice of Next's GetServerSidePropsContext this helper needs: req.headers, req.cookies, req.url, res.setHeader, plus params and query. Your real getServerSideProps context satisfies it.
Need server-side branching as well? Pass an inner handler. It receives the datafile, the context, and the evaluated flags as real Flag objects, and its props are merged with avsbBootstrap:
// pages/pricing.tsximport { getServerSideAvsb } from '@avsbhq/next/pages'export const getServerSideProps = getServerSideAvsb( { sdkKey: process.env.AVSB_SDK_KEY ?? '' }, async ({ flags }) => ({ props: { showBanner: flags['show-banner']?.isEnabled() ?? false }, }),)Spread avsbBootstrap into the provider
// pages/_app.tsximport { AvsbProvider } from '@avsbhq/next'import type { AppProps } from 'next/app'export default function App({ Component, pageProps }: AppProps) { const { avsbBootstrap, ...rest } = pageProps return ( <AvsbProvider {...avsbBootstrap}> <Component {...rest} /> </AvsbProvider> )}avsbBootstrap is exactly the provider's props:
import type { EvalContext, FlagDatafile, SerializedFlagMap } from '@avsbhq/next'interface AvsbPagesBootstrap { sdkKey: string /** The context the server evaluated. */ context: EvalContext /** The datafile, so the browser makes no request before first paint. */ bootstrap: FlagDatafile /** The evaluated values, so the hydration render matches the HTML. */ serverFlags: SerializedFlagMap}The provider stays mounted across client navigations, and pages that do not use getServerSideAvsb still work: the client fetches the datafile itself and reads answer once it lands. Only the first paint of those pages can show defaults.
Read a flag
// components/HeroSection.tsximport { useStringFlag, useExposure } from '@avsbhq/next'import type { Flag } from '@avsbhq/next'export function HeroSection() { const hero: Flag<string> = useStringFlag('homepage_hero', 'control') useExposure('homepage_hero') if (hero.value === 'variant-a') return <HeroVariantA /> if (hero.value === 'variant-b') return <HeroVariantB /> return <HeroControl />}Reads return a Flag<T> object, never a bare value, and always take a default: it is what you get before the SDK is ready, when the key does not exist, and when the flag's declared type does not match the getter. useExposure is what records that the visitor saw the variation, once per flag per mounted view.
Track a conversion
// components/CheckoutButton.tsximport { useTrack } from '@avsbhq/next'export function CheckoutButton() { const track = useTrack() return ( <button type="button" onClick={() => track('checkout_started', { value: 99.0 })}> Proceed to checkout </button> )}Identify a visitor
Call identify from an event handler or an effect, never during render: it re-buckets every flag, and a render must not do that.
// components/PostLoginCallback.tsximport { useEffect } from 'react'import { useIdentify } from '@avsbhq/next'export function PostLoginCallback({ userId, plan }: { userId: string; plan: string }) { const identify = useIdentify() useEffect(() => { identify({ kind: 'user', key: userId, plan }) }, [identify, userId, plan]) return null}Better still, hand the identity to contextFrom in getServerSideAvsb so the server evaluates the signed-in visitor in the first place.
Waiting for flags on a page without server props
// components/FlagGatedSection.tsximport { useFlagReady } from '@avsbhq/next'export function FlagGatedSection() { const ready = useFlagReady() if (!ready) return <Skeleton /> return <ActualContent />}useFlagReady() means flags are readable now. useAvsbStatus() adds status and degraded when you want to tell "still loading" apart from "serving a stale datafile after a failed refresh".
Testing
@avsbhq/react/testing renders the real provider around a real client built from a flags map, so a component test exercises the same code path production does:
// components/HeroSection.test.tsximport { render, screen } from '@testing-library/react'import { AvsbTestProvider } from '@avsbhq/react/testing'import { HeroSection } from './HeroSection'test('renders variant A when the flag says so', () => { render( <AvsbTestProvider flags={{ 'homepage_hero': 'variant-a' }}> <HeroSection /> </AvsbTestProvider>, ) expect(screen.getByRole('heading')).toHaveTextContent('Variant A')})What changed in 1.x
| Change | What to do |
|---|---|
getServerSideAvsb returns props.avsbBootstrap (an object), not props.__avsbBootstrap (a string). | Spread it into AvsbProvider in _app.tsx. |
AvsbProvider requires sdkKey. | Spread avsbBootstrap, which carries it. |
| Reads no longer fire an exposure per render. | Add useExposure(flagKey) where the variation is shown, or exposure counts drop to zero. |
The client entry is built with a 'use client' banner. | You no longer need a wrapper file to import the provider. |
What's next
- Next.js (App Router) if you are migrating.
- Multi-context identity: target users by organization, device, or your own context kinds.
- SDK installation: the hook surface,
Flag<T>, and every evaluation source.