Node.js SDK Reference
TL;DR: Install
@featureflip/node-sdk, callclient.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.
Requirements
Section titled “Requirements”- Node.js 20.19.0+
Installation
Section titled “Installation”npm install @featureflip/node-sdkConfiguration
Section titled “Configuration”The NodeFeatureflipConfig interface accepts the following options:
| Option | Type | Default | Description |
|---|---|---|---|
sdkKey | string | required | SDK key from the Featureflip dashboard |
baseUrl | string | "https://eval.featureflip.io" | Base URL of the Evaluation API |
streaming | boolean | true | Enable SSE for real-time flag updates |
pollInterval | number | 30000 | Polling interval in milliseconds (used when streaming is disabled) |
flushInterval | number | 30000 | Interval in milliseconds between event flush batches |
flushBatchSize | number | 100 | Maximum number of events per flush batch |
initTimeout | number | 10000 | Maximum time in milliseconds to wait for initialization |
maxStreamRetries | number | 5 | Maximum SSE reconnection attempts before falling back to polling |
Example configuration
Section titled “Example configuration”const client = FeatureflipClient.get({ sdkKey: 'sdk-dev-abc123', streaming: true, pollInterval: 60_000, flushInterval: 15_000, flushBatchSize: 50, initTimeout: 5_000,});Initialization
Section titled “Initialization”FeatureflipClient.get()
Section titled “FeatureflipClient.get()”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.
FeatureflipClient.create()
Section titled “FeatureflipClient.create()”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.
isInitialized
Section titled “isInitialized”client.isInitialized: booleanA read-only property that returns true once the client has successfully loaded flag data.
Evaluating Flags
Section titled “Evaluating Flags”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.
boolVariation()
Section titled “boolVariation()”const enabled = client.boolVariation('dark-mode', { user_id: 'u-123' }, false);Signature:
boolVariation(key: string, context: EvaluationContext, defaultValue: boolean): booleanstringVariation()
Section titled “stringVariation()”const plan = client.stringVariation('pricing-tier', { user_id: 'u-123' }, 'free');Signature:
stringVariation(key: string, context: EvaluationContext, defaultValue: string): stringnumberVariation()
Section titled “numberVariation()”const limit = client.numberVariation('rate-limit', { user_id: 'u-123' }, 100);Signature:
numberVariation(key: string, context: EvaluationContext, defaultValue: number): numberjsonVariation()
Section titled “jsonVariation()”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): TEvaluation Details
Section titled “Evaluation Details”Use variationDetail() when you need to understand why a particular value was returned.
variationDetail()
Section titled “variationDetail()”const detail = client.variationDetail('dark-mode', { user_id: 'u-123' }, false);
console.log(detail.value); // trueconsole.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:
| Property | Type | Description |
|---|---|---|
value | T | The evaluated flag value |
reason | EvaluationReason | Why this value was returned |
variationKey | string | undefined | The key of the matched variation |
ruleId | string | undefined | The ID of the matched targeting rule |
EvaluationReason
Section titled “EvaluationReason”| Value | Description |
|---|---|
'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 |
Event Tracking
Section titled “Event Tracking”track()
Section titled “track()”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>): voidEvents are batched and flushed automatically based on flushInterval and flushBatchSize.
identify()
Section titled “identify()”Sends user attributes for segment building and analytics.
Signature:
identify(context: EvaluationContext): voidLifetime
Section titled “Lifetime”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 refcount1. - Subsequent
get()calls with the same SDK key return a new handle and increment the refcount. Theconfigargument 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 onSIGTERM. 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.
Cleanup
Section titled “Cleanup”close()
Section titled “close()”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.
flush()
Section titled “flush()”Forces all buffered events to be sent to the server immediately.
await client.flush();Signature:
flush(): Promise<void>Testing
Section titled “Testing”FeatureflipClient.forTesting()
Section titled “FeatureflipClient.forTesting()”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 valuesclient.boolVariation('dark-mode', {}, false); // trueclient.stringVariation('pricing-tier', {}, 'free'); // 'enterprise'Signature:
static forTesting(flags: Record<string, unknown>): FeatureflipClientThe test client is fully initialized immediately and returns the provided values regardless of the evaluation context.
EvaluationContext
Section titled “EvaluationContext”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.
EvaluationDetail<T>
Section titled “EvaluationDetail<T>”interface EvaluationDetail<T = unknown> { value: T; variationKey?: string; reason: EvaluationReason; ruleId?: string;}NodeFeatureflipConfig
Section titled “NodeFeatureflipConfig”interface NodeFeatureflipConfig { sdkKey: string; baseUrl?: string; streaming?: boolean; pollInterval?: number; flushInterval?: number; flushBatchSize?: number; initTimeout?: number; maxStreamRetries?: number;}Differences from @featureflip/sdk
Section titled “Differences from @featureflip/sdk”The Node SDK is a thin wrapper around @featureflip/sdk. Key differences:
@featureflip/node-sdk | @featureflip/sdk | |
|---|---|---|
| Platform setup | Automatic (Node.js) | Manual (createNodePlatform() or createBrowserPlatform()) |
| Base URL | Defaults to https://eval.featureflip.io | Required |
| Static factory | FeatureflipClient.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.
See also
Section titled “See also”- Node.js Quickstart — get started in under 5 minutes
- Targeting & Segments — control which users see which variation
- Rollout Strategies — gradual percentage-based rollouts
- Evaluation API — REST API reference
- SDK Overview — compare server-side and client-side SDKs