@avsbhq/next
v1.0.0
Published
Next.js 15+ integration for the [A vs B](https://app.avsb.cloud) platform.
Downloads
176
Readme
@avsbhq/next
Next.js 15+ integration for the A vs B platform.
Full App Router support with React Server Components (RSC), server actions, and the middleware layer. Pages Router is supported via a separate subpath. Built on @avsbhq/react — all React hooks are re-exported so you only need this package, not @avsbhq/react separately.
1. Install
npm install @avsbhq/nextNext.js 15 or later and React 18 or later are required as peer dependencies. Node.js 18 or later.
2. Quickstart
App Router
Step 1 — Server component fetches datafile and injects bootstrap:
// app/layout.tsx — a React Server Component
import { getDatafile, AvsbHydrator } from '@avsbhq/next/server'
import { Providers } from './Providers'
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const ctx = { kind: 'user' as const, key: cookies().get('uid')?.value ?? 'anon' }
const datafile = await getDatafile(process.env.AVSB_SDK_KEY!, { cdnHost: 'https://cdn.avsb.cloud' })
return (
<html>
<body>
<AvsbHydrator datafile={datafile} context={ctx} />
<Providers>{children}</Providers>
</body>
</html>
)
}Step 2 — Client provider reads from the injected bootstrap:
// app/Providers.tsx
'use client'
import { AvsbProvider } from '@avsbhq/next'
export function Providers({ children }: { children: React.ReactNode }) {
// No sdkKey needed — reads from window.__AVSB_BOOTSTRAP__ written by AvsbHydrator
return <AvsbProvider>{children}</AvsbProvider>
}Step 3 — Use flags anywhere:
// app/CheckoutButton.tsx
'use client'
import { useBoolFlag, useTrack } from '@avsbhq/next'
export function CheckoutButton() {
const flag = useBoolFlag('new-checkout-flow', false)
const track = useTrack()
return (
<button onClick={() => track('checkout_clicked')}>
{flag.isEnabled() ? 'New checkout' : 'Checkout'}
</button>
)
}Server component evaluation (no client round-trip):
// app/pricing/page.tsx — RSC
import { getDatafile, evaluateFlagServer } from '@avsbhq/next/server'
export default async function PricingPage() {
const ctx = { kind: 'user' as const, key: getCurrentUserId() }
const datafile = await getDatafile(process.env.AVSB_SDK_KEY!)
const flag = evaluateFlagServer(datafile, ctx, 'pricing-experiment', 'control')
if (flag.variationKey === 'usage-based') return <UsageBasedPricing />
return <DefaultPricing />
}3. SDK keys
@avsbhq/next uses both server and client keys depending on context:
| Context | Key type | Variable |
|---|---|---|
| getDatafile, AvsbHydrator, evaluateFlagServer | Server key (sdk-server-...) | AVSB_SDK_KEY |
| <AvsbProvider> client-side | Client key (sdk-client-...) | NEXT_PUBLIC_AVSB_SDK_KEY |
When using the AvsbHydrator pattern (recommended), the client-side provider bootstraps from the injected datafile and never fetches independently. You only need AVSB_SDK_KEY (server).
For independent client-side fetching (no SSR bootstrap):
<AvsbProvider sdkKey={process.env.NEXT_PUBLIC_AVSB_SDK_KEY!} context={ctx}>4. Identity
All @avsbhq/react identity hooks are re-exported directly:
import { useIdentify, useAlias, useReset } from '@avsbhq/next'For server-side identity in RSCs, pass the context directly to evaluateFlagServer or AvsbHydrator:
const ctx = {
kind: 'user' as const,
key: session.userId,
plan: session.plan,
}
const flag = evaluateFlagServer(datafile, ctx, 'new-feature', false)5. Multi-context
// Server component
const ctx = {
kind: 'multi' as const,
user: { kind: 'user' as const, key: userId, plan: 'pro' },
organization: { kind: 'organization' as const, key: orgId, tier: 'enterprise' },
}
const datafile = await getDatafile(process.env.AVSB_SDK_KEY!)
const flag = evaluateFlagServer(datafile, ctx, 'enterprise-dashboard', false)// Client component
const identify = useIdentify()
identify({
kind: 'multi',
user: { kind: 'user', key: userId, plan: 'pro' },
organization: { kind: 'organization', key: orgId, tier: 'enterprise' },
})6. Reading flags
In RSCs (server-side, no client state)
import { getDatafile, evaluateFlagServer } from '@avsbhq/next/server'
const datafile = await getDatafile(process.env.AVSB_SDK_KEY!)
const flag = evaluateFlagServer(datafile, ctx, 'my-flag', false)
// flag is Flag<boolean> — same shape as client-side hooksevaluateFlagServer is a pure function — no network, no state, no side effects. Call it as many times as needed within a single render.
In client components
All @avsbhq/react hooks are available:
import { useBoolFlag, useStringFlag, useNumberFlag, useJsonFlag, useFlag, useFlagValue, useAllFlags } from '@avsbhq/next'See @avsbhq/react documentation for full hook descriptions.
7. Tracking events
'use client'
import { useTrack } from '@avsbhq/next'
const track = useTrack()
track('purchase_completed', { value: 199.0, properties: { plan: 'annual' } })Server-side tracking via @avsbhq/node:
// app/_lib/avsbServer.ts
import { AvsbServer } from '@avsbhq/node'
export const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! })
await server.onReady()
// In a server action or route handler:
server.forUser(ctx).track('form_submitted', { value: 1 })8. Error handling
Client-side
import { useAvsbStatus } from '@avsbhq/next'
const { status, error } = useAvsbStatus()
if (status === 'error') return <ErrorBanner />Server-side
getDatafile throws on network failure. Wrap in try/catch and fall back to a pre-cached datafile or serve defaults:
let datafile: FlagDatafile
try {
datafile = await getDatafile(process.env.AVSB_SDK_KEY!)
} catch {
datafile = fallbackDatafile // stale cached datafile
}9. SSR / hydration
The AvsbHydrator server component writes the evaluated datafile and context to window.__AVSB_BOOTSTRAP__ via an inline <script type="application/json"> tag. <AvsbProvider> on the client reads this blob on mount and resolves onReady() immediately with source: 'bootstrap' — no network request on the client.
This ensures the same flag values are used on the server render and the initial client render, preventing React hydration mismatches.
// Full pattern:
<AvsbHydrator datafile={datafile} context={ctx} />
<AvsbProvider>{children}</AvsbProvider>
// The provider reads window.__AVSB_BOOTSTRAP__ automaticallyIf you need to render a component tree on the server with the correct flag values AND hydrate on the client, use AvsbServerProvider:
import { AvsbServerProvider } from '@avsbhq/next/server'
<AvsbServerProvider datafile={datafile} context={ctx}>
<MyServerTree />
<AvsbHydrator datafile={datafile} context={ctx} />
</AvsbServerProvider>10. Graceful shutdown
Client-side: <AvsbProvider> flushes queued events on unmount automatically. For root-level providers that never unmount, use:
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', () => { void client?.flush() })
}Server-side (@avsbhq/node instance):
process.on('SIGTERM', async () => {
await server.close()
process.exit(0)
})11. Testing
import { render, screen } from '@testing-library/react'
import { AvsbTestProvider } from '@avsbhq/test'
import { CheckoutButton } from './CheckoutButton'
test('renders new checkout when flag on', () => {
render(
<AvsbTestProvider mockFlags={{ 'new-checkout-flow': true }}>
<CheckoutButton />
</AvsbTestProvider>
)
expect(screen.getByRole('button')).toHaveTextContent('New checkout')
})For RSC testing, call evaluateFlagServer directly in your test with a mock datafile:
import { evaluateFlagServer } from '@avsbhq/next/server'
import { createTestDatafile } from '@avsbhq/test'
const datafile = createTestDatafile({ 'my-flag': true })
const flag = evaluateFlagServer(datafile, { kind: 'user', key: 'u_1' }, 'my-flag', false)
expect(flag.value).toBe(true)12. App Router middleware
// middleware.ts (project root)
import { avsbNextAppMiddleware } from '@avsbhq/next/middleware'
import { server } from './lib/avsbServer'
export const middleware = avsbNextAppMiddleware({
server,
contextFrom: (req) => ({
kind: 'user' as const,
key: req.cookies.get('uid')?.value ?? 'anon',
}),
})
export const config = { matcher: '/(.*)' }The middleware opens an AsyncLocalStorage scope so you can call getRequestClient() from @avsbhq/utils anywhere in the request chain without passing the client explicitly.
13. Pages Router
// pages/checkout.tsx
import { getServerSideAvsb } from '@avsbhq/next/pages'
import { server } from '../lib/avsbServer'
export const getServerSideProps = getServerSideAvsb({
server,
contextFrom: (ctx) => ({
kind: 'user' as const,
key: ctx.req.cookies.uid ?? 'anon',
}),
})
export default function CheckoutPage({ avsbBootstrap }) {
return (
<AvsbProvider bootstrap={avsbBootstrap}>
<CheckoutContent />
</AvsbProvider>
)
}14. Migration
From LaunchDarkly Next.js
| LaunchDarkly Next.js | @avsbhq/next |
|---|---|
| withLDProvider(options)(App) | <AvsbProvider> in app/Providers.tsx |
| getLDBootstrapData | getDatafile + AvsbHydrator |
| useLDClient() | useAvsbClient() |
| useFlags() | useAllFlags() |
| useLDFlag('key', default) | useFlag('key', default).value |
| LDProvider + createInspectors | <AvsbProvider onError={...} logger={...}> |
Key differences:
- The
AvsbHydratorserver component eliminates the need for separate SSR bootstrap wiring — inject it once in your root layout. - Client components only need
import ... from '@avsbhq/next'— no separate@avsbhq/reactinstall. evaluateFlagServeris a pure function usable in any RSC without provider context.
From Statsig Next.js
| Statsig Next.js | @avsbhq/next |
|---|---|
| StatsigProvider | <AvsbProvider> |
| useGate('gate') | useBoolFlag('gate', false).isEnabled() |
| useStatsigClient() | useAvsbClient() |
| prefetchStatsig | getDatafile (server) + AvsbHydrator |
