Next.js 15 App Router integration
This guide assumes a Next.js 15 project using the App Router with React Server Components enabled. By the end you'll have zero-latency flag reads in both Server Components and Client Components, with no hydration flicker, using @avsbhq/next and @avsbhq/react.
Install
1npm install @avsbhq/next@^1 @avsbhq/react@^1 @avsbhq/browser@^1@avsbhq/next provides the server-side hydrator and RSC-safe evaluator. @avsbhq/react provides client-side hooks.@avsbhq/browser is a peer dependency of both.
Obtain your SDK key
Navigate to your feature flag project in the A vs B platform, open Settings > Environments, and copy the SDK key for the environment you want to target. Add it to your environment file:
1AVSB_SDK_KEY=sdk_production_xxxxxxxxxxxxxxxxNEXT_PUBLIC_ prefix are server-only.Bootstrap flags in the root layout
AvsbHydrator is a Server Component from @avsbhq/next/server. It fetches the datafile at request time and injects a bootstrap blob into the page as a <script type="application/json"> tag. The client provider reads this blob on mount, which means flags evaluate synchronously on the first client render.
1import { AvsbHydrator } from '@avsbhq/next/server'2import { Providers } from './providers'3
4export default async function RootLayout({5 children,6}: {7 children: React.ReactNode8}) {9 // Optionally derive a context from cookies / headers here.10 // Pass undefined to hydrate without a context (client identifies later).11 return (12 <html lang="en">13 <body>14 <AvsbHydrator sdkKey={process.env.AVSB_SDK_KEY!} />15 <Providers>{children}</Providers>16 </body>17 </html>18 )19}1'use client'2import { AvsbProvider } from '@avsbhq/next'3
4export function Providers({ children }: { children: React.ReactNode }) {5 // No sdkKey needed — reads from window.__AVSB_BOOTSTRAP__ written by AvsbHydrator.6 return <AvsbProvider>{children}</AvsbProvider>7}Read a flag in a Server Component
Use evaluateFlagServer from @avsbhq/next/server to evaluate flags statically on the server without a running client instance. Pass the context you build from the incoming request (cookies, headers, or auth session).
1import { getDatafile, evaluateFlagServer } from '@avsbhq/next/server'2import type { EvalContext } from '@avsbhq/core'3
4export default async function DashboardPage() {5 const datafile = await getDatafile(process.env.AVSB_SDK_KEY!)6
7 // Build context from session/cookies in a real app.8 const ctx: EvalContext = { kind: 'user', key: 'u_preview', plan: 'pro' }9
10 const showNewDashboard = evaluateFlagServer(11 datafile,12 ctx,13 'new-dashboard',14 false15 )16
17 return showNewDashboard.value ? <NewDashboard /> : <LegacyDashboard />18}evaluateFlagServer is synchronous and CPU-only — it never makes a network call. The datafile is fetched once and cached by Next.js fetch deduplication across the request lifetime.Read a flag in a Client Component
Client Components use the same useFlag hook from @avsbhq/react. Because AvsbHydrator already bootstrapped the datafile, the hook returns the correct value on the first render with no loading flash.
1'use client'2import { useFlag, useIdentify } from '@avsbhq/react'3import type { EvalContext } from '@avsbhq/core'4
5export function Navbar({ userId }: { userId: string }) {6 // Identify the user once you have their ID on the client.7 const identify = useIdentify()8 const ctx: EvalContext = { kind: 'user', key: userId }9 identify(ctx)10
11 const newNavFlag = useFlag('new-navigation', false)12
13 return newNavFlag.value ? <NewNav /> : <LegacyNav />14}Track an event
1'use client'2import { useTrack } from '@avsbhq/react'3
4export function PurchaseButton() {5 const track = useTrack()6
7 function handleClick() {8 track('purchase', { value: 49.99, properties: { plan: 'pro' } })9 }10
11 return <button onClick={handleClick} type="button">Buy now</button>12}Identify a user
Call identifywhen the user's identity becomes known (e.g. after login). This replaces the entire bound context and rehashes all bucketing immediately.
1'use client'2import { useIdentify } from '@avsbhq/react'3
4export function AuthCallback({ userId, plan }: { userId: string; plan: string }) {5 const identify = useIdentify()6
7 // Call once after login resolves.8 identify({ kind: 'user', key: userId, plan })9
10 return null11}SSR and hydration
The App Router integration eliminates hydration mismatch by sharing the same datafile between the server render and the client mount. AvsbHydrator serialises the datafile into the HTML stream; AvsbProvider reads it from window.__AVSB_BOOTSTRAP__ before the first client render. The datafile is never fetched again on the client unless the polling interval elapses.
To pass a user context into the hydrated datafile — for example to pre-evaluate flags for a logged-in user — pass a context prop to AvsbHydrator:
1import { cookies } from 'next/headers'2import { AvsbHydrator } from '@avsbhq/next/server'3
4export default async function RootLayout({ children }: { children: React.ReactNode }) {5 const uid = (await cookies()).get('uid')?.value ?? 'anon'6 const ctx = { kind: 'user' as const, key: uid }7
8 return (9 <html lang="en">10 <body>11 <AvsbHydrator sdkKey={process.env.AVSB_SDK_KEY!} context={ctx} />12 {children}13 </body>14 </html>15 )16}Edge middleware for flag-based routing
Use the Next.js middleware adapter from @avsbhq/next/middleware to make flag decisions in the edge runtime before the request reaches a page. This is useful for A/B redirects or geofenced rollouts.
1import { avsbNextAppMiddleware } from '@avsbhq/next/middleware'2import { AvsbServer } from '@avsbhq/node'3
4const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! })5
6export const middleware = avsbNextAppMiddleware({7 server,8 contextFrom: (req) => ({9 kind: 'user',10 key: req.cookies.get('uid')?.value ?? 'anon',11 }),12})13
14export const config = { matcher: '/((?!_next|api).*) ' }avsbNextAppMiddleware lives in @avsbhq/utils/middleware/nextApp internally and may be renamed before V1. The export path shown above is the intended stable public surface.Graceful shutdown
The AvsbProvider calls client.close() automatically when it unmounts. For server instances created in middleware, handle SIGTERM in your custom server:
1process.on('SIGTERM', async () => {2 await server.close()3 process.exit(0)4})Testing
1import { AvsbTestProvider } from '@avsbhq/test'2import { TestData } from '@avsbhq/test'3import { render } from '@testing-library/react'4import { MyComponent } from './MyComponent'5
6const td = TestData.flag('new-navigation').booleanFlag().fallthroughVariation(true)7
8render(9 <AvsbTestProvider flags={[td.build()]}>10 <MyComponent />11 </AvsbTestProvider>12)What's next
- Multi-context identity — target users by organization, device, or custom context kinds.
- Sticky bucketing — guarantee consistent variant assignment across sessions.
@avsbhq/nextAPI reference@avsbhq/reactAPI reference