/Docs

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.

1

Install

bash
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.

2

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:

.env.local
bash
1AVSB_SDK_KEY=sdk_production_xxxxxxxxxxxxxxxx
Info
Server SDK keys are safe to store in server-side environment variables. Do not expose them to the browser. Next.js ensures variables without the NEXT_PUBLIC_ prefix are server-only.
3

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.

app/layout.tsx
tsx
1import { AvsbHydrator } from '@avsbhq/next/server'
2import { Providers } from './providers'
3
4export default async function RootLayout({
5 children,
6}: {
7 children: React.ReactNode
8}) {
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}
app/providers.tsx
tsx
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}
4

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).

app/dashboard/page.tsx
tsx
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 false
15 )
16
17 return showNewDashboard.value ? <NewDashboard /> : <LegacyDashboard />
18}
Tip
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.
5

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.

app/_components/Navbar/Navbar.tsx
tsx
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}
6

Track an event

tsx
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}
7

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.

tsx
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 null
11}

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:

app/layout.tsx (with context)
tsx
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.

middleware.ts
ts
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).*) ' }
Warning
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:

ts
1process.on('SIGTERM', async () => {
2 await server.close()
3 process.exit(0)
4})

Testing

tsx
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