Abrova
☰ Docs menu · Next.js
Coming soon. Abrova Flags is coming soon. This page describes how it will work; it can’t be used yet.

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.

Next.js 0.2.0Node.js 18+React 18+Last updated: 27 Oct 2026
Server Entry

AbrovaFlags with the server secret, for Server Components, Route Handlers, Server Actions and middleware

Client Entry

AbrovaFlagsProvider and useAbrovaFlags with the public key, for Client Components

Standalone Client

AbrovaFlagsClient for browser code without React

Rules Stay on the Server

The browser receives values for this user. Targeting rules never reach it

Typed Getters

getBoolean, getString, getNumber and getJson, each with a default

TypeScript

Full type definitions included

Abrova Flags Next.js SDK v0.2.0.

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.

Requirements.

Node.js 18 or newer. React 18 or newer for the provider and the hook.

Installation#

bash
# 1. .npmrc in your project root: @abrova packages come from Abrova Center
@abrova:registry=https://center.abrova.ir/api/packages/abrova/npm/
bash
# 2. Install
npm install @abrova/flags-nextjs
# or
yarn add @abrova/flags-nextjs

Two entry points#

@abrova/flags-nextjs/server

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
@abrova/flags-nextjs/client

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 Requires Server Secret.

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:

typescript
// 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
});
tsx
// 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.

MethodReturns
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)#

Client-Safe.

The client entry uses the public API key only. AbrovaFlagsClient throws if you pass a server secret.

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

tsx
// 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.

typescript
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();
MethodWhat 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. setAttributes and the attributes of identify replace the whole set; they do not merge.
  • User change: the previous user's values are dropped at once. Until the new values arrive, isReady is false and getters return your defaults.
  • Anonymous id: each browser gets a random id (a UUID), kept in localStorage and 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 localStorage and 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 on refresh(). Polling is off unless you set refreshIntervalSeconds (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.
Each fetch counts as one evaluation.

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.

Decide on the server when you can

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

Otherwise render after mount

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

Use one id on both sides

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

Signed-out visitors

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

tsx
'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#

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

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.x0.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 + setUserAttributesidentify(userId, attributes), setUserId, setAttributes. They return a Promise and fetch new values
Client: getAll() returned flags with rulesgetAll() returns this user's values only
isLoading from the hookisReady
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 AbrovaFlags from @abrova/flags-nextjs no longer works: change it to @abrova/flags-nextjs/server.
  • On the server, rename userAttributes to attributes. A context that still uses userAttributes is evaluated without attributes.
  • Rollouts use a new bucketing method, so users are assigned to rollouts and variants again once.

Best Practices#

Separate server and client keys

Use ABROVA_FLAGS_API_KEY and ABROVA_FLAGS_SERVER_SECRET on the server and NEXT_PUBLIC_ABROVA_FLAGS_API_KEY in client components

One server instance

Create the server AbrovaFlags once in a module and await flags.init() where you read flags. Later calls return at once

Use the Provider pattern

Wrap your app with AbrovaFlagsProvider for consistent client-side flag access

Handle the not-ready state

Use isReady from the hook to show a skeleton until values are available

Prefer the server for first paint

Flags that change what the page looks like on load are best decided in a Server Component

Was this page helpful?
Report a problem