Next.js SDK
One package with two entry points. The server entry evaluates flags locally with your server secret; the client entry gives the browser values for the current user with the public key only.
AbrovaFlags with the server secret, for Server Components, Route Handlers, Server Actions and middleware
AbrovaFlagsProvider and useAbrovaFlags with the public key, for Client Components
AbrovaFlagsClient for browser code without React
The browser receives values for this user. Targeting rules never reach it
getBoolean, getString, getNumber and getJson, each with a default
Full type definitions included
This page describes @abrova/flags-nextjs 0.2.0. It is a breaking release: the server SDK moved to @abrova/flags-nextjs/server, the browser no longer downloads rules, and typed getters replace getValue. Coming from 0.1.x? See Upgrading from 0.1.x at the end of this page.
Node.js 18 or newer. React 18 or newer for the provider and the hook.
Installation#
# 1. .npmrc in your project root: @abrova packages come from Abrova Center @abrova:registry=https://center.abrova.ir/api/packages/abrova/npm/
# 2. Install npm install @abrova/flags-nextjs # or yarn add @abrova/flags-nextjs
Two entry points#
Server Components, Route Handlers, Server Actions, middleware
- API key and server secret
- Downloads the rules and evaluates locally
- You pass the user context with every call
- Not counted as evaluations
Client Components and other browser code
- Public API key only
- Abrova Flags evaluates; the browser gets values
- The client remembers the user you identify
- Each fetch counts as one evaluation
The package root (@abrova/flags-nextjs) is the same as /client. The server SDK is only in /server, so it cannot end up in a browser bundle, and it refuses to start in a browser.
Server-Side (App Router)#
Server-side usage requires both apiKey and serverSecret. The server secret can read every targeting rule of your project. Never expose it in client-side code or in an environment variable prefixed with NEXT_PUBLIC_.
Create one instance per process, in its own module:
// lib/flags.ts
import { AbrovaFlags } from '@abrova/flags-nextjs/server';
export const flags = new AbrovaFlags({
apiKey: process.env.ABROVA_FLAGS_API_KEY!,
serverSecret: process.env.ABROVA_FLAGS_SERVER_SECRET!,
environment: 'production', // leave out to use the Default values
});// app/page.tsx (Server Component)
import { flags } from '@/lib/flags';
export default async function Page() {
await flags.init(); // the first call downloads the rules, later calls return at once
const user = { userId: 'user-123', attributes: { plan: 'pro', country: 'IR' } };
const newCheckout = flags.isEnabled('new-checkout', user);
const theme = flags.getString('theme', user, 'light');
return (
<main data-theme={theme}>
{newCheckout ? <NewCheckout /> : <OldCheckout />}
</main>
);
}The context is passed with every call: userId, anonymousId and attributes, all optional. Percentage rollouts and variant splits use userId, or anonymousId when there is no user id.
| Method | Returns |
|---|---|
| init() | Downloads the rules and starts refreshing them every 30 seconds. Resolves after the first download or after initTimeoutMs (5 seconds). Never rejects |
| isEnabled(key, context?, defaultValue = false) | true when the flag is served on for this context |
| getBoolean / getString / getNumber / getJson(key, context, defaultValue) | The typed value, or the default |
| getDetail(key, context, defaultValue, valueType?) | key, value, enabled, variant, reason, version and valueType |
| getAll(context?) | Every flag evaluated for the context, by key |
| getFlagKeys() | The keys of the loaded flags |
| refresh() | Fetch the rules now |
| close() | Stop refreshing |
| on('ready' | 'update' | 'error', fn) | Listen for events. Returns a function that removes the listener |
The server entry behaves like the Node.js SDK: the same options (pollIntervalSeconds, initTimeoutMs, sendUsage, debug, logger), the same reasons and the same error handling. It also runs in the Edge runtime.
Client-Side (React Hook)#
The client entry uses the public API key only. AbrovaFlagsClient throws if you pass a server secret.
// app/providers.tsx
'use client';
import { AbrovaFlagsProvider } from '@abrova/flags-nextjs/client';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<AbrovaFlagsProvider config={{
apiKey: process.env.NEXT_PUBLIC_ABROVA_FLAGS_API_KEY!,
environment: 'production', // leave out to use the Default values
}}>
{children}
</AbrovaFlagsProvider>
);
}Using the Hook
// components/Checkout.tsx
'use client';
import { useEffect } from 'react';
import { useAbrovaFlags } from '@abrova/flags-nextjs/client';
export function Checkout({ userId, plan }: { userId: string | null; plan: string }) {
const { isReady, isEnabled, getString, identify } = useAbrovaFlags();
useEffect(() => {
identify(userId, { plan }); // null means signed out
}, [identify, userId, plan]);
if (!isReady) return <Skeleton />;
return isEnabled('new-checkout')
? <NewCheckout theme={getString('theme', 'light')} />
: <OldCheckout />;
}The hook returns client, isReady, the getters (isEnabled, getBoolean, getString, getNumber, getJson, getDetail, getAll) and the actions (identify, setUserId, setAttributes, reset, resetAnonymousId, refresh). The functions keep the same identity between renders, so they are safe in effect dependency lists. Components re-render when the values change.
Call identify in an effect, not in the component body.
Standalone Client (No React)#
For browser code outside React, use AbrovaFlagsClient directly.
import { AbrovaFlagsClient } from '@abrova/flags-nextjs/client';
const client = new AbrovaFlagsClient({
apiKey: 'ab_live_xxx',
environment: 'production',
userId: 'user-123',
attributes: { plan: 'pro' },
});
await client.init();
const enabled = client.isEnabled('new-checkout');
const theme = client.getString('theme', 'light');
const detail = client.getDetail('theme', 'light', 'string');
// { key, value, enabled, variant, reason, version, valueType }
// Sign-out: clears the user, the attributes and the cached values
await client.reset();
// Stop timers and network work
client.close();| Method | What it does |
|---|---|
| init() | Serves cached values and fetches fresh ones. Resolves after the first answer or after initTimeoutMs (5 seconds) |
| isReady() | true when values for the current user are available |
| isEnabled(key, defaultValue = false) | true when the flag is served on for this user |
| getBoolean / getString / getNumber / getJson(key, defaultValue) | The typed value, or the default |
| getDetail(key, defaultValue, valueType?) | The full result with the reason |
| getAll() | Every value for the current user, by key |
| identify(userId | null, attributes?) | Sets the user and, when given, replaces the attributes. Fetches new values |
| setUserId(userId | null) | Changes the user id only |
| setAttributes(attributes) | Replaces the whole set of attributes |
| reset() | Sign-out: clears the user, the attributes and the cached values. Keeps the anonymous id |
| resetAnonymousId() | Creates a new anonymous id |
| refresh() | Fetch now |
| on('ready' | 'change' | 'error', fn) | Listen for events. Returns a function that removes the listener |
| getUserId() / getAttributes() / getAnonymousId() | The current context |
| close() | Stops timers and network work |
How the client behaves#
- Context changes fetch new values after a short pause (250 ms). A call that changes nothing sends no request.
setAttributesand the attributes ofidentifyreplace the whole set; they do not merge. - User change: the previous user's values are dropped at once. Until the new values arrive,
isReadyisfalseand getters return your defaults. - Anonymous id: each browser gets a random id (a UUID), kept in
localStorageand sent with every request, so percentage rollouts work for signed-out visitors too. If storage is blocked, the id lives in memory for the page. - Cache: the last values are kept in
localStorageand served at the next visit, before the network answers. Offline or on errors, the last values keep being served. - Refresh: on
init, on context changes, when the tab becomes visible and the last fetch is older than 15 minutes, and onrefresh(). Polling is off unless you setrefreshIntervalSeconds(minimum 60). - Errors: rate limits (429) and server errors are retried with a growing delay (up to 5 minutes). 401, 403 and 404 (wrong key, Flags not enabled, unknown environment) stop automatic fetches until
refresh(). - Attributes are strings, numbers, booleans, null, or arrays of those. No nested objects. At most 100 attributes; a string holds at most 1,024 characters.
Every request the client entry sends for values is counted as one flag evaluation, whatever the number of flags it returns (100,000 evaluations are free every month; the price above that is set at launch). This is why polling is off by default. Rule downloads and local evaluations of the server entry are not counted.
Client options: apiKey, environment, userId, attributes, flagKeys, refreshIntervalSeconds, initTimeoutMs, storage, debug and logger.
Server rendering and hydration#
The client entry has no values while a page is rendered on the server: isReady is false and every getter returns the default you pass. In the browser, cached values from an earlier visit are available from the first render. The HTML from the server and the first render in the browser can therefore differ.
Evaluate the flag in a Server Component with the server entry and pass the result down as a prop. There is no flicker and no extra request from the browser
For flags read in Client Components, render the flag-dependent part only after the component has mounted and isReady is true. Show a skeleton or the default until then
For a signed-in user, pass the same userId on the server and to identify() in the browser. Both sides then give the same rollout and variant
The browser uses its own anonymous id, which the server does not know. For a signed-out visitor, decide a rollout flag in one place: on the server with an id you keep in a cookie, or in the browser
'use client';
import { useEffect, useState } from 'react';
import { useAbrovaFlags } from '@abrova/flags-nextjs/client';
export function PromoBanner() {
const { isReady, isEnabled } = useAbrovaFlags();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted || !isReady) return null;
return isEnabled('promo-banner') ? <Banner /> : null;
}Environment Variables#
# .env.local # Server-side only (NEVER expose these to the client) ABROVA_FLAGS_API_KEY=ab_live_xxx ABROVA_FLAGS_SERVER_SECRET=ab_srv_xxx # For client-side usage (safe to expose) NEXT_PUBLIC_ABROVA_FLAGS_API_KEY=ab_live_xxx # Note: Do NOT create NEXT_PUBLIC_ABROVA_FLAGS_SERVER_SECRET
Never prefix your server secret with NEXT_PUBLIC_. Only the API key should be exposed to the client.
Upgrading from 0.1.x#
Update the package and change these imports and calls:
| 0.1.x | 0.2.0 |
|---|---|
| import { AbrovaFlags } from '@abrova/flags-nextjs' | import { AbrovaFlags } from '@abrova/flags-nextjs/server' |
| AbrovaFlagsProvider, useAbrovaFlags, AbrovaFlagsClient from '@abrova/flags-nextjs' | Same names from '@abrova/flags-nextjs/client'. The package root still works |
| Server: isEnabled(key, { userId, userAttributes }) | isEnabled(key, { userId, attributes }) |
| getValue(key, ..., default) | getBoolean / getString / getNumber / getJson |
| evaluate(key, ...) | getDetail(key, ..., default), with reason and version |
| Client: setUserId + setUserAttributes | identify(userId, attributes), setUserId, setAttributes. They return a Promise and fetch new values |
| Client: getAll() returned flags with rules | getAll() returns this user's values only |
| isLoading from the hook | isReady |
| dispose() | close() |
- Upgrade browser and server together: in 0.2.0 Abrova Flags evaluates for the browser, and a 0.1.x client entry stops receiving flags.
- The root import no longer exports the server SDK. Importing
AbrovaFlagsfrom@abrova/flags-nextjsno longer works: change it to@abrova/flags-nextjs/server. - On the server, rename
userAttributestoattributes. A context that still usesuserAttributesis evaluated without attributes. - Rollouts use a new bucketing method, so users are assigned to rollouts and variants again once.
Best Practices#
Use ABROVA_FLAGS_API_KEY and ABROVA_FLAGS_SERVER_SECRET on the server and NEXT_PUBLIC_ABROVA_FLAGS_API_KEY in client components
Create the server AbrovaFlags once in a module and await flags.init() where you read flags. Later calls return at once
Wrap your app with AbrovaFlagsProvider for consistent client-side flag access
Use isReady from the hook to show a skeleton until values are available
Flags that change what the page looks like on load are best decided in a Server Component