On this page

For AI agents: a documentation index is available at /llms.txt. Append .md to any page URL for markdown, or send Accept: text/markdown.

Node Server SDK

Statsig's next-gen Node Server SDK built on our Server Core framework

Migrating from the legacy Node SDK? Refer to our Migration Guide.

Set up the SDK

  1. Install the SDK

    shell
    npm i @statsig/statsig-node-core
    

    The Node SDK is pre-built and compiled for different operating systems and CPU architectures. Package managers resolve the correct version automatically.

  2. Initialize the SDK

    After installation, initialize the SDK using a Server Secret Key from the Statsig console.

    Keep Server Secret Keys private. If you expose one, you can disable and recreate it in the Statsig console.

    The optional options parameter accepts a StatsigOptions object to customize the SDK.

    jsx
    // Basic initialization
    import { Statsig, StatsigUser } from '@statsig/statsig-node-core';
    //Or, in common JS, const { Statsig, StatsigUser } = require('@statsig/statsig-node-core');
    
    const statsig = new Statsig("secret-key");
    await statsig.initialize();
    
    // or with StatsigOptions
    const options: StatsigOptions = { environment: "staging" };
    
    const statsigWithOptions = new Statsig("secret-key", options);
    await statsigWithOptions.initialize();
    

    initialize performs a network request. After initialize completes, virtually all SDK operations are synchronous (refer to Evaluating Feature Gates in the Statsig SDK). The SDK fetches updates from Statsig in the background independently of API calls.

Working with the SDK

Checking a feature flag/gate

After you initialize the SDK, you can fetch a Feature Gate. Feature Gates create logic branches in code that you can roll out to different users from the Statsig Console. Gates are always CLOSED or OFF (equivalent to return false;) by default.

All APIs require a user object (refer to Statsig user). For example:

jsx
const user = new StatsigUser({ userID: "a-user" });

if (statsig.checkGate(user, "a_gate")) {
    // Gate is on, enable new feature
} else {
    // Gate is off
}

Reading a dynamic config

Feature Gates are useful for on/off switches with optional user targeting. To send different values (strings, numbers, and similar types) to clients based on user attributes such as country, use Dynamic Configs. The API is similar to Feature Gates, but returns a full JSON object configurable on the server, from which you can fetch typed parameters.

jsx
// Get the dynamic config
const config = statsig.getDynamicConfig(user, "a_config");

// Get typed values using the getValue() method
const itemName = config.getValue("product_name", "Awesome Product v1");
const price = config.getValue("price", 10.0);
const shouldDiscount = config.getValue("discount", false);

// Or access the entire value object directly
const value = config.value;

Getting a layer/experiment

Use Layers/Experiments to run A/B/n experiments. Two APIs are available. Statsig recommends Layers because they make parameters reusable and support mutually exclusive experiments.

jsx
// Or, via individual experiments
const titleExp = statsig.getExperiment(user, "new_user_promo_title");
const priceExp = statsig.getExperiment(user, "new_user_promo_price");

const experimentTitle = titleExp.getValue("title", "Welcome to Statsig!");
const experimentDiscount = priceExp.getValue("discount", 0.1);

// Get values via Layer
const layer = statsig.getLayer(user, "user_promo_experiments");
const title = layer.getValue("title", "Welcome to Statsig!");
const discount = layer.getValue("discount", 0.1);

Retrieving feature gate metadata

In certain scenarios, you may need more information about a gate evaluation than a boolean value. For additional metadata about the evaluation, use the Get Feature Gate API, which returns a FeatureGate object:

jsx
const gate = statsig.getFeatureGate(statsigUser, "example_gate")
console.log(gate.rule_id)
console.log(gate.value)

Parameter stores

Use Parameter Stores when you want to define a parameter without deciding whether it should be a Feature Gate, Experiment, or Dynamic Config. Parameter Stores let you change the parameter type at any point in the Statsig console without a new deployment. Parameter Stores are optional, but parameterizing your application provides future flexibility and allows non-technical Statsig users to turn parameters into experiments.

jsx
const paramStore = statsig.getParameterStore(statsigUser, "my_parameters")
const paramStoreValue = paramStore.getValue('my_parameter_value')

Logging an event

To track custom events, call the Log Event API. Specify the user, event name, and an optional value or metadata object:

jsx
statsig.logEvent(
  user,
  "add_to_cart",
  null,
  {
    price: "9.99",
    item_name: "diet_coke_48_pack"
  }
);

Learn more about identifying users, group analytics, and best practices for logging events in the logging events guide.

Sending events to Log Explorer

You can forward logs to Logs Explorer for convenient analysis using the Forward Log Line Event API. This lets you include custom metadata and event values with each log.

jsx
const user = new StatsigUser({ userID: "a-user", custom: {
    service: "my-service",
    pod: "my-pod",
    namespace: "my-namespace",
    container: "my-container",
    // ...include any service-specific metadata
} });

// levels: trace, debug, info, log, warn, error
statsig.forwardLogLineEvent(user, "warn", "script failed to load", {
  cusom_metadata: "script_name:my-script"
  // ... include any event-specific metadata
});

Using shared instance

To create a single Statsig instance accessible globally throughout your codebase, use the shared instance functionality, which provides a singleton pattern:

jsx
// Create a shared instance that can be accessed globally
const statsig = Statsig.newShared("secret-key");
await statsig.initialize();

// Access the shared instance from anywhere in your code
const sharedStatsig = Statsig.shared();
const isFeatureEnabled = sharedStatsig.checkGate(user, "feature_name");

// Check if a shared instance exists
if (Statsig.hasSharedInstance()) {
  // Use the shared instance
}

// Remove the shared instance when no longer needed
Statsig.removeShared();

The shared instance helps when multiple parts of your codebase need Statsig without passing an instance between them.

  • Statsig.newShared(sdkKey, options): Creates a new shared instance of Statsig that you can access globally
  • Statsig.shared(): Returns the shared instance
  • Statsig.hasSharedInstance(): Checks if a shared instance exists (useful when the shared instance may not be ready yet)
  • Statsig.removeShared(): Removes the shared instance (useful when you want to switch to a new shared instance)

hasSharedInstance() and removeShared() are helpful in specific scenarios but aren't required in most use cases where the shared instance is set up near the top of your application.

Also note that only one shared instance can exist at a time. Attempting to create a second shared instance results in an error.

Manual exposures

By default, the SDK automatically logs an exposure event when you check a gate, get a config, get an experiment, or call get() on a parameter in a layer. To delay exposure logging (for example, to log only after the user actually uses the feature), use manual exposures.

All main SDK functions (checkGate, getDynamicConfig, getExperiment, getLayer) accept an optional disableExposureLogging parameter. When set to true, the SDK doesn't automatically log an exposure. You can then log the exposure manually at a later time:

jsx
const result = statsig.checkGate(aUser, 'a_gate_name', {disableExposureLogging: true});
jsx
statsig.manuallyLogGateExposure(aUser, 'a_gate_name');

Statsig user

The StatsigUser object represents a user in Statsig. You must provide a userID or at least one of the customIDs to identify the user.

When calling APIs that require a user, pass as much information as possible. Complete user information lets you take advantage of advanced gate and config conditions (like country or OS/browser-level checks) and correctly measure the impact of experiments on metrics and events. As explained in why an ID is always required for server SDKs, you must provide at least one identifier (userID or customID) to ensure a consistent experience for a given user.

In addition to userID, the top-level fields on StatsigUser are: email, ip, userAgent, country, locale, and appVersion. You can also pass any key-value pairs in an object or dictionary to the custom field and create targeting based on them.

Private attributes

Private attributes are user attributes that Statsig uses for evaluation but doesn't forward to any integrations. They are useful for PII or sensitive data that you don't want to send to third-party services.

typescript
const user = new StatsigUser({
  userID: "a-user-id",
  email: "user@example.com",
  ip: "192.168.1.1",
  userAgent: "Mozilla/5.0...",
  country: "US",
  locale: "en_US",
  appVersion: "1.0.0",
  custom: {
    // Custom fields
    plan: "premium",
    age: 25
  },
  customIDs: {
    // Custom ID types
    stableID: "stable-id-123"
  },
  privateAttributes: {
    // Private attributes not forwarded to integrations
    email: "private@example.com"
  }
});

Statsig options

You can pass an optional options parameter in addition to sdkKey during initialization to customize the Statsig client.

Proxy and custom network routing

Use proxyConfig if your service needs a standard outbound HTTP proxy. Use specAdaptersConfig if you are routing spec downloads through Statsig Forward Proxy or another custom spec source.

typescript
// Example usage:
const options: StatsigOptions = {
  environment: "staging",
  initTimeoutMs: 3000,
  proxyConfig: {
    proxyHost: "proxy.example.com",
    proxyPort: 8080,
    // proxyAuth: set if authentication is required
    proxyProtocol: "https",
    caCertPath: "/etc/ssl/certs/corporate-ca.pem",
  },
};

const statsig = new Statsig("secret-key", options);
await statsig.initialize();

Set caCertPath when your environment requires a custom PEM CA bundle for outbound TLS.

Routing endpoints through a forward proxy

To send Statsig network traffic through your own proxy or CDN, override the endpoint options. These options don't all take the same kind of value:

  • specsUrl and idListsUrl take a full endpoint URL. The SDK requests each URL as-is.
  • downloadIdListFileApi takes an origin only (scheme and host, no path). Per-file download URLs are dynamic. Each one carries a unique file ID and signature embedded in the ID list manifest, so the SDK can't use a fixed URL. Instead, it takes each per-file path from the manifest and appends it to this origin.
typescript
const options: StatsigOptions = {
  // Full endpoint URLs, used as-is:
  specsUrl: "https://proxy.example.com/v1/download_config_specs",
  idListsUrl: "https://proxy.example.com/v1/get_id_lists",
  // Origin only. The SDK appends each manifest file path:
  downloadIdListFileApi: "https://proxy.example.com",
};

const statsig = new Statsig("secret-key", options);
await statsig.initialize();

Set downloadIdListFileApi to an origin only, such as https://proxy.example.com. If you include a path such as /v1/download_id_list_file, the SDK appends the manifest path on top of it, producing a doubled path such as .../v1/download_id_list_file/v1/download_id_list_file/<fileId> and a 401 response.

Overriding only specsUrl and idListsUrl routes the ID list manifest through your proxy, but individual ID list file downloads still go to the Statsig CDN. Set downloadIdListFileApi to route those file downloads through your proxy as well.

Shutting Statsig down

Statsig batches and periodically flushes events. To flush all logged events before shutdown, call shutdown() before your app or server shuts down:

jsx
await statsig.shutdown();

Client SDK bootstrapping | SSR

If you use the Statsig client SDK in a browser or mobile app, you can bootstrap the client SDK with values from the server SDK to avoid a network request on the client. This is useful for server-side rendering (SSR) or to reduce network requests on the client.

Client initialize response

The Node Core SDK provides a method to generate a client initialize response that you can use to bootstrap client SDKs without requiring network requests.

typescript
// Get client initialize response for a user
const values = statsig.getClientInitializeResponse(user, options);

// Pass values to a client SDK to initialize without a network request

SDK event subscriptions

The Statsig SDK provides an event subscription system that lets you listen for evaluation events and lifecycle events in real time. This is useful for debugging, analytics, custom logging, and integration with external systems.

Supported events

The SDK supports subscribing to the following evaluation events:

  • gate_evaluated - Fired when a feature gate is evaluated for a user
  • dynamic_config_evaluated - Fired when a dynamic config is retrieved for a user
  • experiment_evaluated - Fired when an experiment is evaluated for a user
  • layer_evaluated - Fired when a layer is evaluated for a user
  • specs_updated - Fired when the SDK updates its cached specs, including where the specs were loaded from
  • "*" - Subscribe to all evaluation events

SDK event data

Each event includes relevant context about the evaluation:

  • Gate Evaluated Events include: gate_name, value (boolean), rule_id, reason
  • Dynamic Config Events include: the full dynamic_config object with values and metadata
  • Experiment Events include: the full experiment object with variant assignment and parameters
  • Layer Events include: the full layer object with allocated experiment and parameters
  • Specs Updated Events include source, source_api, and values metadata, where values.time is the timestamp of the last update to the project in the Statsig console

Use cases

Event subscriptions are particularly useful for:

  • Debugging: Monitor which features are being evaluated and their results
  • Analytics: Track feature usage patterns and user segments
  • Custom Logging: Send evaluation data to your own logging systems
  • Integration: Forward events to external analytics or monitoring tools
  • Testing: Verify that features are being evaluated as expected

Best practices

  • Clean up subscriptions: Always unsubscribe when you no longer need to listen for events to prevent memory leaks
  • Handle event data carefully: Event objects may contain sensitive user information depending on your configuration
  • Use specific event types: Subscribe to specific events rather than "*" when possible for better performance
  • Avoid heavy processing: Keep event handlers lightweight to avoid impacting SDK performance
javascript
const statsig = new Statsig('server-secret-key');

// Subscribe to gate evaluation events
const gateSubId = statsig.subscribe('gate_evaluated', (event) => {
  console.log('Gate evaluated:', {
    gateName: event.gate_name,
    value: event.value,
    ruleId: event.rule_id,
    reason: event.reason
  });
});

// Subscribe to dynamic config evaluation events
const configSubId = statsig.subscribe('dynamic_config_evaluated', (event) => {
  console.log('Config evaluated:', {
    configName: event.dynamic_config.name,
    values: event.dynamic_config.value
  });
});

// Subscribe to experiment evaluation events
const experimentSubId = statsig.subscribe('experiment_evaluated', (event) => {
  console.log('Experiment evaluated:', {
    experimentName: event.experiment.name,
    groupName: event.experiment.group_name,
    parameters: event.experiment.value
  });
});

// Subscribe to layer evaluation events
const layerSubId = statsig.subscribe('layer_evaluated', (event) => {
  console.log('Layer evaluated:', {
    layerName: event.layer.name,
    allocatedExperiment: event.layer.allocated_experiment_name,
    parameters: event.layer.value
  });
});

// Subscribe to specs updated events
const specsUpdatedSubId = statsig.subscribe('specs_updated', (event) => {
  console.log('Specs updated:', {
    source: event.data.source,
    sourceApi: event.data.source_api,
    projectLastUpdated: event.data.values.time,
  });
});

// Subscribe to all events
const allEventsSubId = statsig.subscribe('*', (event) => {
  console.log('Event received:', event.event_name, event);
});

// Unsubscribe from specific event types
statsig.unsubscribe('gate_evaluated');
statsig.unsubscribe('specs_updated');

// Unsubscribe using subscription ID
statsig.unsubscribeById(configSubId);

// Unsubscribe from all events
statsig.unsubscribeAll();

Local overrides

Local Overrides let you override the values of gates, configs, experiments, and layers for testing. This is useful for local development or testing when you want to force a specific value without changing the configuration in the Statsig console.

jsx
// Overrides the given gate to the specified value
Statsig.overrideGate("a_gate_name", true);

// Optional third parameter, overrides the gate only for a given ID
Statsig.overrideGate("a_gate_name", true, "userID-123");

Persistent storage

The Persistent Storage interface lets you implement custom storage for user-specific configurations. This enables you to persist user assignments across sessions, ensuring consistent experiment groups when a user returns. This is useful for client-side A/B testing where users must always receive the same variant.

typescript
export interface PersistentStorage {
  load: (key: string) => UserPersistedValues | null;
  save: (key: string, config_name: string, data: StickyValues) => void;
  delete: (key: string, config_name: string) => void;
}

export interface StickyValues {
  value: boolean;
  json_value: Record<string, unknown>;
  rule_id: string;
  group_name: string | null;
  secondary_exposures: SecondaryExposure[];
  undelegated_secondary_exposures: SecondaryExposure[];
  config_delegate: string | null;
  explicit_parameters: string[] | null;
  time: number;
  configVersion?: number | undefined;
}

export type UserPersistedValues = Record<string, StickyValues>;

export interface SecondaryExposure {
  gate: string;
  gateValue: string;
  ruleId: string;
}

Usage example

typescript
import { PersistentStorage, StickyValues, UserPersistedValues } from '@statsig/statsig-node-core';

class MyPersistentStorage implements PersistentStorage {
  private storage = new Map<string, UserPersistedValues>();
  constructor() {
      this.load = this.load.bind(this);
      this.save = this.save.bind(this);
      this.delete = this.delete.bind(this);
  }

  load(key: string): UserPersistedValues | null {
    return this.storage.get(key) || null;
  }

  save(key: string, config_name: string, data: StickyValues): void {
    const existing = this.storage.get(key) || {};
    existing[config_name] = data;
    this.storage.set(key, existing);
  }

  delete(key: string, config_name: string): void {
    const existing = this.storage.get(key);
    if (existing) {
      delete existing[config_name];
      this.storage.set(key, existing);
    }
  }
  
  getUserPersistedValue(user: StatsigUser, idType: string): UserPersistedValues | null {
    const storageKey = this.getStorageKey(user, idType);
    if (storageKey !== null) {
      return this.load(storageKey);
    }
    return null;
  }

  private getStorageKey(user: StatsigUser, idType: string): string | null {
    const lowerCaseIdType = idType.toLowerCase();

    if (lowerCaseIdType === "user_id" || lowerCaseIdType === "userid") {
      const id = user.userID;
      return id ? `${id}:userID` : null;
    } else if (user.customIDs) {
      const id = user.customIDs[idType];
      return id ? `$\{id\}:${idType}` : null;
    }

    return null;
  }

}

Statsig added persistent storage support in version 0.6.1 of the Node.js SDK.

Data store

Use a DataStore to cache configuration specs and ID lists in Redis or another store you control. The SDK reads cached data during initialization and can keep it updated from Statsig. You implement storage access; the SDK manages cache keys, downloads, and ID-list serialization.

ID-list DataStore support is available in @statsig/statsig-node-core@0.23.2-rc.2610201817, but not stable version 0.23.2. Enable enableIdLists: true and the ID-list capabilities described here.

DataStore interface

Implement the asynchronous callbacks for your storage client:

Configure the adapter

Treat keys and values as opaque. Save the values passed to set and return them from get without changing their contents. You don't need to construct keys, fetch ID-list files, or parse list members.

This example wraps an existing cache client. It stores {result,time} as JSON and unwraps that envelope when reading. Use the same format in every reader and writer. Use a separate connection per SDK instance, or manage shared connections outside the adapter's lifecycle callbacks.

typescript
import {
  Statsig,
  type DataStore,
  type DataStoreResponse,
} from "@statsig/statsig-node-core";

interface AsyncCache {
  connect(): Promise<unknown>;
  quit(): Promise<unknown>;
  get(key: string): Promise<string | null>;
  set(key: string, value: string): Promise<unknown>;
}

function createDataStore(
  cache: AsyncCache,
  pollCache = true,
  writeIdLists = false,
): DataStore {
  return {
    async initialize() {
      await cache.connect();
    },
    async shutdown() {
      await cache.quit();
    },
    async get(key): Promise<DataStoreResponse> {
      const value = await cache.get(key);
      return value === null ? {} : (JSON.parse(value) as DataStoreResponse);
    },
    async set(key, value, time) {
      await cache.set(key, JSON.stringify({ result: value, time }));
    },
    async supportsFeature(feature) {
      if (feature === "id_list_snapshot_write") return writeIdLists;
      return feature === "config_specs" || feature === "id_list_snapshot";
    },
    async supportPollingUpdatesFor() {
      return pollCache;
    },
  };
}

async function startClient(
  key: string,
  cache: AsyncCache,
  pollCache = true,
  writeIdLists = false,
) {
  const statsig = new Statsig(key, {
    dataStore: createDataStore(cache, pollCache, writeIdLists),
    enableIdLists: true,
  });
  const result = await statsig.initialize();
  if (!result.isSuccess) {
    // Application policy: stop rather than wait for background recovery.
    await statsig.shutdown();
    throw new Error("Statsig initialization failed");
  }
  return statsig;
}

Pass your server SDK key and cache client to startClient. Await statsig.shutdown() during application shutdown. Keep SDK keys and cached data private, and configure connection timeouts and access controls in your storage client.

Select a publisher and readers

Run one writer service to keep the shared store up to date. With the example adapter, call startClient(key, cache, false, true): initialize from the cache, poll Statsig HTTP for updates, and save supported data back to the store.

In application processes, call startClient(key, cache) with the same SDK key and cache namespace. These clients initialize from the cache and poll it for updates without writing ID lists. They can still fall back to HTTP if cached data is missing or invalid.

The writeIdLists argument controls ID-list writes only. The example still implements set for configuration specs. For a completely read-only adapter, omit both set and setBytes. Keep the writer running: the storage service doesn't fetch Statsig updates itself.

Enable ID-list snapshots and choose polling

Use supportsFeature to opt into each data type:

ID-list storage includes a lookup of the available lists and a separate entry for each list's complete membership. The SDK maintains these entries through get and set; your adapter only stores and retrieves the supplied keys and values.

For ID lists, supportPollingUpdatesFor("id_list_snapshot") defaults to true after read support is enabled. Returning false selects HTTP for scheduled updates, but initialization still tries the DataStore first. The example applies one polling choice to both config specs and ID lists. idListsSyncIntervalMs controls the ID-list polling interval and defaults to 60 seconds.

Initialization, refresh, and failure behavior

  • Missing, invalid, incomplete, or failed ID-list reads trigger HTTP fallback. The SDK installs a complete snapshot, not a mix of partial cached and downloaded lists.
  • If initialization fails after cache and HTTP attempts, initialize() returns isSuccess: false. A running client can recover through background polling; the example instead shuts down on failure.
  • Later refresh failures keep the last known good in-memory lists. DataStore support isn't a fail-closed or no-network mode.
  • The SDK doesn't expire valid ID-list snapshots using time. Keep the writer running and configure expiry in your store if needed.
  • With writes enabled, a successful HTTP refresh saves validated ID lists. Write failures don't discard the downloaded in-memory update. Unchanged snapshots can skip writes, so don't rely on every poll to renew a cache TTL.
  • ID-list updates span multiple storage entries; DataStore support doesn't make those writes transactional.
  • Automatic DataStore ID-list recovery after fork() isn't supported. Create the storage connection, adapter, and SDK in the child process instead.

Custom output logger

The Output Logger interface lets you customize how the SDK logs messages, enabling integration with your own logging system and control over log verbosity.

Observability client

The Observability Client interface lets you monitor the health of the SDK by integrating with your own observability systems. This enables you to track metrics, errors, and performance data. For more information on the metrics Statsig SDKs emit, refer to the Monitoring documentation.

typescript
export interface ObservabilityClient {
  initialize?: () => void;
  increment?: (metricName: string, value: number, tags: Record<string, string>) => void;
  gauge?: (metricName: string, value: number, tags: Record<string, string>) => void;
  dist?: (metricName: string, value: number, tags: Record<string, string>) => void;
  error?: (tag: string, error: string) => void;
}

FAQs

How do I run experiments for logged out users

Common problems while installing

  1. Seeing SSL Error

The binary files require certain SSL versions.

shell
// Try run this
apt-get update && apt-get install libcurl4-openssl-dev -y && rm -rf /var/lib/apt/lists/*
  1. Docker build failing with platform-specific dependencies

When building in Docker (Linux environment), the build may fail if your local package-lock.json or yarn.lock contains platform-specific dependencies for macOS. This happens because npm install on Mac pulls down Apple-specific variants, but Docker tries to use those locked dependencies on Linux.

Solution: Either install the Linux-specific variant during your Docker build step:

dockerfile
RUN npm install @statsig/statsig-node-core-linux-x64-gnu

Or add both platform variants as dependencies in your package.json:

json
"dependencies": {
  "@statsig/statsig-node-core": "X.Y.Z", // Common (Required)
  "@statsig/statsig-node-core-darwin-arm64": "X.Y.Z", // Mac Specific
  "@statsig/statsig-node-core-linux-x64-gnu": "X.Y.Z" // Linux Specific
}

Reference

Was this helpful?