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

Node.js SDK

Server-side SDK with API key and server secret. Downloads your flag rules, keeps them fresh and evaluates every flag locally for the context you pass per request.

Node.js 0.2.0Node.js 18+Last updated: 27 Oct 2026
Server-Side Only.

The Node.js SDK requires both apiKey and serverSecret. The server secret can read every targeting rule of your project: never expose it in a browser or an app bundle. For client apps, use the Flutter, React Native, Android, iOS or Next.js client SDKs.

Abrova Flags Node.js SDK v0.2.0.

This page describes @abrova/flags-node 0.2.0. It is a breaking release: typed getters replace getValue, the context field is attributes, and rules refresh on their own. Coming from 0.1.x? See Upgrading from 0.1.x at the end of this page.

Requirements.

Node.js 18 or newer (the SDK uses the built-in fetch).

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-node
# or
yarn add @abrova/flags-node

Initialization#

Create one instance for the process and reuse it.

typescript
import { AbrovaFlags } from '@abrova/flags-node';

const flags = new AbrovaFlags({
  apiKey: process.env.ABROVA_FLAGS_API_KEY!,             // ab_live_xxx
  serverSecret: process.env.ABROVA_FLAGS_SERVER_SECRET!, // ab_srv_xxx, backend only
  environment: 'production',                             // leave out to use the Default values
});

await flags.init();

init() downloads the rules and starts refreshing them every 30 seconds. It resolves after the first download, or after initTimeoutMs (5 seconds) if the network is slow, and it never rejects. Until the rules arrive, every getter returns your default with reason NOT_READY.

The constructor throws when apiKey or serverSecret is missing, and when it runs in a browser.

Usage#

There is no shared user state on a server. Pass the context with every call.

typescript
const user = {
  userId: 'user-123',
  attributes: { plan: 'pro', country: 'IR' },
};

if (flags.isEnabled('new-checkout', user)) {
  // New checkout logic
}

const theme = flags.getString('theme', user, 'light');
const limit = flags.getNumber('upload-limit-mb', user, 10);
const dark = flags.getBoolean('dark-mode', user, false);
const banner = flags.getJson('banner', user, { text: '' });

Evaluation is local and synchronous: no network call per flag check.

Context

typescript
interface EvaluationContext {
  userId?: string | null;      // the signed-in user's stable id
  anonymousId?: string | null; // for signed-out visitors, for example an id your client sends
  attributes?: Record<string, string | number | boolean | null | Array<string | number | boolean | null>>;
}

Percentage rollouts and variant splits use userId, or anonymousId when there is no user id. Without either, a partial rollout or a split is served off with reason NO_BUCKETING_ID. Rules on userId or anonymousId read these two fields, never an attribute of the same name.

Getters

MethodReturns
isEnabled(key, context?, defaultValue = false)true when the flag is served on for this context
getBoolean(key, context, defaultValue)Value of a boolean flag, or the default
getString(key, context, defaultValue)Value of a string flag, or the default
getNumber(key, context, defaultValue)Value of a number flag, or the default
getJson(key, context, defaultValue)Value of a json flag, 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

Details and reasons

typescript
const d = flags.getDetail('theme', user, 'light', 'string');
console.log(d.value, d.variant, d.reason); // "dark" "dark" "VARIANT"

Pass the fourth argument ('boolean', 'string', 'number' or 'json') to get your default and TYPE_MISMATCH when the flag has another type. Without it, getDetail returns the value as it is.

ReasonMeaning
ONThe flag is on and has no variants. The value is true
VARIANTThe flag is on and a variant was served
DISABLEDThe flag is off in this environment. The flag off value is served
NOT_TARGETEDThe targeting rules did not match the context. The off value is served
OUTSIDE_ROLLOUTThe user is outside the rollout percentage. The off value is served
PREREQUISITE_FAILEDA flag this one depends on did not give the required result. The off value is served
NO_BUCKETING_IDA rollout or split needed a userId or anonymousId and the context has neither
ERRORThe flag could not be evaluated. The off value is served
FLAG_NOT_FOUNDNo such flag. Your default is returned
NOT_READYRules are not loaded yet. Your default is returned
TYPE_MISMATCHFor example getString on a boolean flag. Your default is returned

isEnabled is true only for ON and VARIANT.

Events#

typescript
const offReady = flags.on('ready', ({ flagCount }) => {
  console.log(`Loaded ${flagCount} flags`);
});

flags.on('update', ({ flagCount }) => {
  console.log(`Rules changed, ${flagCount} flags`);
});

flags.on('error', ({ status, code, message }) => {
  console.error(`Abrova Flags error ${status ?? ''} ${code ?? ''}: ${message}`);
});

// on() returns a function that removes the listener
offReady();

Express Middleware#

typescript
import express from 'express';
import { AbrovaFlags } from '@abrova/flags-node';

const app = express();
const flags = new AbrovaFlags({
  apiKey: process.env.ABROVA_FLAGS_API_KEY!,
  serverSecret: process.env.ABROVA_FLAGS_SERVER_SECRET!,
  environment: 'production',
});

await flags.init();

// Pass the user context per request
app.get('/api/feature', (req, res) => {
  const enabled = flags.isEnabled('my-feature', {
    userId: req.user.id,
    attributes: { plan: req.user.plan },
  });

  res.json({ featureEnabled: enabled });
});

const server = app.listen(3000);

// Stop refreshing on shutdown
process.on('SIGTERM', async () => {
  await flags.close();
  server.close();
});

Refresh and errors#

  • Automatic refresh: the SDK checks for new rules every pollIntervalSeconds (30 by default). An unchanged answer costs almost nothing, so you do not need to call refresh() on a timer.
  • Failures: a failed refresh keeps the last good rules. Rate limits (429) and server errors are retried with a growing delay (up to 5 minutes), following the wait time the service asks for.
  • 401, 403 or 404 (wrong key or secret, a server address that is not on the secret's IP allow-list, unknown environment): refreshing stops and an error event fires. Fix the configuration, then call flags.refresh(), which also restarts the refresh.
  • flags.refresh() fetches the rules now. flags.close() stops refreshing and sends pending usage counts.
Not counted as evaluations.

Downloading rules and evaluating flags locally are not counted as evaluations. Set sendUsage: true if you want the evaluation charts in Abrova Console to include this server: the SDK then sends totals per flag about once a minute. They contain no user ids and are not billed.

Environment Overrides#

Pass an environment in the config to get the flags with that environment's overrides applied.

typescript
const flags = new AbrovaFlags({
  apiKey: process.env.ABROVA_FLAGS_API_KEY!,
  serverSecret: process.env.ABROVA_FLAGS_SERVER_SECRET!,
  environment: process.env.NODE_ENV === 'production'
    ? 'production'
    : 'development',
});

await flags.init();

const maintenance = flags.isEnabled('maintenance-mode', { userId: 'user-123' });

The environment key must exist in your project. An unknown key is answered with 404: the SDK logs an error and getters return your defaults.

Configuration Options#

OptionTypeDefaultDescription
apiKeystringrequiredYour Abrova API key (ab_live_xxx)
serverSecretstringrequiredYour server secret (ab_srv_xxx)
environmentstringnoneEnvironment key (production, staging, development or your own). Leave out to use the Default values
pollIntervalSecondsnumber30Seconds between rule refreshes. Minimum 10. 0 turns the refresh off
initTimeoutMsnumber5000How long init() waits for the first download
sendUsagebooleanfalseSend evaluation totals per flag for the charts in Abrova Console. Not billed, no user ids
debugbooleanfalseEnable debug logging
loggerobjectconsoleYour own logger with debug, info, warn and error methods

Upgrading from 0.1.x#

Update the package and change these calls:

0.1.x0.2.0
isEnabled(key, { userId, userAttributes })isEnabled(key, { userId, attributes })
getValue(key, ctx, default)getBoolean / getString / getNumber / getJson(key, ctx, default)
evaluate(key, ctx)getDetail(key, ctx, default), which adds reason and version
getAll() returned raw flagsgetAll(ctx) returns evaluated results; getFlagKeys() lists the keys
init(callback)init(), plus on('ready' | 'update' | 'error', fn)
dispose() / reset()close()
refresh() on your own timerNot needed. Rules refresh every 30 seconds
  • Rename userAttributes to attributes everywhere. A context that still uses userAttributes is evaluated without attributes.
  • Flag dependencies and segments are now evaluated by the SDK, so results match the service.
  • Rollouts use a new bucketing method, so users are assigned to rollouts and variants again once.

Best Practices#

Never expose server secret

Keep the server secret in environment variables. Never commit it to version control or expose it in client code

Initialize once

Create a single AbrovaFlags instance and reuse it across your application

Pass user context per request

Always pass userId (or anonymousId) and attributes in each call for accurate targeting and rollout

Always give a sensible default

The default is what your users get before the rules are loaded and when a flag is removed

Close on shutdown

Call close() when the process stops so the refresh timer ends and usage counts are sent

Was this page helpful?
Report a problem