Skip to content

Node.js SDK Reference

TL;DR: Install @featureflip/node-sdk, call client.boolVariation('flag-key', { user_id: 'u-123' }, false), and you’re evaluating flags from any Node service.

The @featureflip/node package is the server-side wrapper around @featureflip/js, tuned specifically for Node.js processes. It pre-wires the Node fetch implementation, sets the SDK’s User-Agent (a header that browser fetch forbids), and configures connection pooling so a single long-lived client can handle a high request rate without leaking sockets. Node 20.19.0 or later is required — earlier versions ship a fetch implementation that doesn’t handle SSE reconnection cleanly.

Use this package on any Node.js workload: Express/Fastify/Koa/Hapi servers, AWS Lambda, Vercel serverless functions, BullMQ workers, NestJS microservices, or scheduled scripts. Feature flags are evaluated locally — there is no per-request network call to Featureflip — so adding flag checks to a hot endpoint costs roughly a Map.get. The streaming connection runs on a background timer and surfaces config updates within milliseconds, so a percentage rollout toggled in the dashboard takes effect across your fleet without a deploy.

The Node SDK is singleton-by-construction: there is no public constructor, only the static factory FeatureflipClient.get(config). Every call with the same SDK key returns a handle pointing at the same underlying shared client. You can call get() from anywhere in your process — inside a request handler, inside a DI container, inside a library — and the SDK guarantees you only ever get one set of background tasks and one SSE streaming connection per key. See the Lifetime section below for the full contract. For advanced targeting rules, pass an evaluation context with arbitrary attributes — anything serializable as JSON works.

View source on GitHub

  • Node.js 20.19.0+
Terminal window
npm install @featureflip/node-sdk

The NodeFeatureflipConfig interface accepts the following options:

OptionTypeDefaultDescription
sdkKeystringrequiredSDK key from the Featureflip dashboard
baseUrlstring"https://eval.featureflip.io"Base URL of the Evaluation API
streamingbooleantrueEnable SSE for real-time flag updates
pollIntervalnumber30000Polling interval in milliseconds (used when streaming is disabled)
flushIntervalnumber30000Interval in milliseconds between event flush batches
flushBatchSizenumber100Maximum number of events per flush batch
initTimeoutnumber10000Maximum time in milliseconds to wait for initialization
maxStreamRetriesnumber5Maximum SSE reconnection attempts before falling back to polling
const client = FeatureflipClient.get({
sdkKey: 'sdk-dev-abc123',
streaming: true,
pollInterval: 60_000,
flushInterval: 15_000,
flushBatchSize: 50,
initTimeout: 5_000,
});

The primary entry point. Synchronously returns a handle; the shared core loads flag configuration in the background.

import { FeatureflipClient } from '@featureflip/node-sdk';
const client = FeatureflipClient.get({
sdkKey: 'sdk-dev-abc123',
});
await client.waitForInitialization();

Calling get() multiple times with the same SDK key returns different handles backed by the same shared client. See Lifetime.

Convenience helper that calls get() and awaits initialization before returning.

const client = await FeatureflipClient.create({
sdkKey: 'sdk-dev-abc123',
});

waitForInitialization() rejects with an error if the fetch does not complete within initTimeout milliseconds.

client.isInitialized: boolean

A read-only property that returns true once the client has successfully loaded flag data.

All evaluation methods accept a flag key, an evaluation context, and a default value. If the flag is not found or an error occurs, the default value is returned.

const enabled = client.boolVariation('dark-mode', { user_id: 'u-123' }, false);

Signature:

boolVariation(key: string, context: EvaluationContext, defaultValue: boolean): boolean
const plan = client.stringVariation('pricing-tier', { user_id: 'u-123' }, 'free');

Signature:

stringVariation(key: string, context: EvaluationContext, defaultValue: string): string
const limit = client.numberVariation('rate-limit', { user_id: 'u-123' }, 100);

Signature:

numberVariation(key: string, context: EvaluationContext, defaultValue: number): number
interface BannerConfig {
text: string;
color: string;
}
const banner = client.jsonVariation<BannerConfig>(
'banner-config',
{ user_id: 'u-123' },
{ text: 'Welcome', color: '#000' },
);

Signature:

jsonVariation<T>(key: string, context: EvaluationContext, defaultValue: T): T

Use variationDetail() when you need to understand why a particular value was returned.

const detail = client.variationDetail('dark-mode', { user_id: 'u-123' }, false);
console.log(detail.value); // true
console.log(detail.reason); // 'RuleMatch'
console.log(detail.ruleId); // 'rule-abc'

Signature:

variationDetail<T>(key: string, context: EvaluationContext, defaultValue: T): EvaluationDetail<T>

Returns an EvaluationDetail<T> object:

PropertyTypeDescription
valueTThe evaluated flag value
reasonEvaluationReasonWhy this value was returned
variationKeystring | undefinedThe key of the matched variation
ruleIdstring | undefinedThe ID of the matched targeting rule
ValueDescription
'RuleMatch'A targeting rule matched the context
'Fallthrough'No rules matched; the fallthrough variation was served
'FlagDisabled'The flag is disabled; the off variation was served
'FlagNotFound'The flag key does not exist
'Error'An error occurred during evaluation

Records a custom analytics event.

client.track('purchase-completed', { user_id: 'u-123' }, {
plan: 'pro',
amount: 29.99,
});

Signature:

track(eventKey: string, context: EvaluationContext, metadata?: Record<string, unknown>): void

Events are batched and flushed automatically based on flushInterval and flushBatchSize.

Sends user attributes for segment building and analytics.

client.identify({ user_id: 'u-123', email: '[email protected]', plan: 'pro' });

Signature:

identify(context: EvaluationContext): void

The Node SDK is singleton-by-construction: you cannot new a client directly, and every FeatureflipClient.get(config) call with the same SDK key returns a handle pointing at one shared background client. This is intentional — a Featureflip client maintains a long-lived SSE streaming connection, a periodic event flush loop, and an in-memory flag store. Opening more than one per SDK key per process wastes server resources and causes update delivery to fan out unpredictably.

The factory refcounts the shared core:

  • The first get() for a given SDK key constructs the shared core, kicks off initialization, and returns a handle with refcount 1.
  • Subsequent get() calls with the same SDK key return a new handle and increment the refcount. The config argument on repeat calls is ignored; if it differs meaningfully from the first call’s config, a warning is logged.
  • handle.close() decrements the refcount. When the refcount reaches zero, the shared core runs its real shutdown: closes the SSE connection, flushes any pending events, and removes itself from the factory cache.
  • Double-closing the same handle is a no-op — the handle tracks its own disposed state, so calling close() twice does not double-decrement the refcount.
  • A different SDK key always constructs an independent shared core. Multi-tenant scenarios — one process serving flags for multiple environments — work naturally.

This makes the SDK safe under any lifetime pattern:

  • Singleton at startup (the recommended pattern): call get() once in your bootstrap code, inject the handle everywhere, close() it on SIGTERM. The refcount stays at 1 for the life of the process.
  • Per-request handler in Express/Fastify/NestJS: calling get() on every request is harmless. Each request gets a fresh handle on the same underlying shared core — no new HTTP fetch, no new SSE connection, no new background timers.
  • Per-scope DI registration: scoped or transient registration is no longer catastrophic. Each scope resolves to a new handle sharing the same core.
  • Serverless warm invocations: a warm Lambda or Vercel Function reuses the cached core across invocations automatically. Cold starts construct a fresh core per process, as expected.

Use FeatureflipClient.forTesting(flags) to obtain a standalone test client that is not registered in the factory cache — each call returns an independent instance with no background connections.

Decrements the refcount on the shared core. When the last handle for a given SDK key is closed, the shared core flushes pending events, closes the SSE connection, stops the polling timer, and removes itself from the factory cache.

await client.close();

Signature:

close(): Promise<void>

Call close() during graceful shutdown (e.g., in a SIGTERM handler) to ensure buffered analytics events are delivered before the process exits. Calling close() on a handle that has already been closed is a no-op — the handle tracks its own state and will not double-decrement the refcount.

Forces all buffered events to be sent to the server immediately.

await client.flush();

Signature:

flush(): Promise<void>

Creates a test client with hardcoded flag values. No network calls are made and no background processes are started.

const client = FeatureflipClient.forTesting({
'dark-mode': true,
'pricing-tier': 'enterprise',
'rate-limit': 500,
});
// Evaluations return the test values
client.boolVariation('dark-mode', {}, false); // true
client.stringVariation('pricing-tier', {}, 'free'); // 'enterprise'

Signature:

static forTesting(flags: Record<string, unknown>): FeatureflipClient

The test client is fully initialized immediately and returns the provided values regardless of the evaluation context.

type EvaluationContext = Record<string, unknown>;

A plain object containing user attributes for flag evaluation. The user_id property is used for percentage rollouts and event tracking.

interface EvaluationDetail<T = unknown> {
value: T;
variationKey?: string;
reason: EvaluationReason;
ruleId?: string;
}
interface NodeFeatureflipConfig {
sdkKey: string;
baseUrl?: string;
streaming?: boolean;
pollInterval?: number;
flushInterval?: number;
flushBatchSize?: number;
initTimeout?: number;
maxStreamRetries?: number;
}

The Node SDK is a thin wrapper around @featureflip/sdk. Key differences:

@featureflip/node-sdk@featureflip/sdk
Platform setupAutomatic (Node.js)Manual (createNodePlatform() or createBrowserPlatform())
Base URLDefaults to https://eval.featureflip.ioRequired
Static factoryFeatureflipClient.create()Not available

Use @featureflip/node-sdk for Node.js applications. Use @featureflip/sdk directly if you need to support both Node.js and browser from the same codebase.