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.
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.
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.
Node.js 18 or newer (the SDK uses the built-in fetch).
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-node # or yarn add @abrova/flags-node
Initialization#
Create one instance for the process and reuse it.
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.
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
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
| Method | Returns |
|---|---|
| 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
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.
| Reason | Meaning |
|---|---|
| ON | The flag is on and has no variants. The value is true |
| VARIANT | The flag is on and a variant was served |
| DISABLED | The flag is off in this environment. The flag off value is served |
| NOT_TARGETED | The targeting rules did not match the context. The off value is served |
| OUTSIDE_ROLLOUT | The user is outside the rollout percentage. The off value is served |
| PREREQUISITE_FAILED | A flag this one depends on did not give the required result. The off value is served |
| NO_BUCKETING_ID | A rollout or split needed a userId or anonymousId and the context has neither |
| ERROR | The flag could not be evaluated. The off value is served |
| FLAG_NOT_FOUND | No such flag. Your default is returned |
| NOT_READY | Rules are not loaded yet. Your default is returned |
| TYPE_MISMATCH | For example getString on a boolean flag. Your default is returned |
isEnabled is true only for ON and VARIANT.
Events#
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#
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 callrefresh()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
errorevent fires. Fix the configuration, then callflags.refresh(), which also restarts the refresh. flags.refresh()fetches the rules now.flags.close()stops refreshing and sends pending usage counts.
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.
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#
| Option | Type | Default | Description |
|---|---|---|---|
| apiKey | string | required | Your Abrova API key (ab_live_xxx) |
| serverSecret | string | required | Your server secret (ab_srv_xxx) |
| environment | string | none | Environment key (production, staging, development or your own). Leave out to use the Default values |
| pollIntervalSeconds | number | 30 | Seconds between rule refreshes. Minimum 10. 0 turns the refresh off |
| initTimeoutMs | number | 5000 | How long init() waits for the first download |
| sendUsage | boolean | false | Send evaluation totals per flag for the charts in Abrova Console. Not billed, no user ids |
| debug | boolean | false | Enable debug logging |
| logger | object | console | Your own logger with debug, info, warn and error methods |
Upgrading from 0.1.x#
Update the package and change these calls:
| 0.1.x | 0.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 flags | getAll(ctx) returns evaluated results; getFlagKeys() lists the keys |
| init(callback) | init(), plus on('ready' | 'update' | 'error', fn) |
| dispose() / reset() | close() |
| refresh() on your own timer | Not needed. Rules refresh every 30 seconds |
- Rename
userAttributestoattributeseverywhere. A context that still usesuserAttributesis 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#
Keep the server secret in environment variables. Never commit it to version control or expose it in client code
Create a single AbrovaFlags instance and reuse it across your application
Always pass userId (or anonymousId) and attributes in each call for accurate targeting and rollout
The default is what your users get before the rules are loaded and when a flag is removed
Call close() when the process stops so the refresh timer ends and usage counts are sent