Server SDKs

Node.js SDK

Evaluate configs and feature flags locally for any user context with the Node.js server SDK, with updates delivered over server sent events

Introduction

The Node.js SDK is a server-side SDK that evaluates configs and their targeting rules. Upon initialization, it retrieves configs and targeting rules from ConfigDirector services. From that point on, it evaluates configs locally for any user context, and receives updates via server sent events (SSE) when configs are updated on the dashboard or via the admin API.

The minimum Node.js version supported is 18.0.

The minimum Deno version supported is 2.0.

The minimum Bun version supported is 1.0.

Installation

The SDK can be installed from NPM: https://www.npmjs.com/package/@configdirector/server-sdk

npm install --save @configdirector/server-sdk

Configure and initialize the client

  1. Create an instance of the client providing your server SDK key. You can retrieve a server SDK key for each environment under SDK Keys in the dashboard's navigation panel.
  2. Initialize the client to initiate its connection lifecycle.
config-director-setup.ts (Initialize the client)
import { createClient } from "@configdirector/server-sdk";

// IMPORTANT: Do not commit the server SDK key to your source code, it is a secret value.
export const client = createClient("YOUR-SERVER-SDK-KEY");
await client.initialize();
Server SDK keys are secret values. Do not commit them to your source code repository. Provide them at runtime via environment variables instead.

Additional configuration options

metadata

The metadata option allows you to provide your application's name and version. These values can be used in targeting rules conditionals. For example, if a certain feature should only be enabled starting with a certain version of your application.

config-director-setup.ts
import { createClient } from "@configdirector/server-sdk";

export const client = createClient("YOUR-SERVER-SDK-KEY", {
  metadata: {
    appName: "YOUR-APP-NAME",
    appVersion: "1.0.2",
  },
});

await client.initialize();

logger

By default, the SDK logs to the console and it is set to log warnings and errors only. You can configure a logger by either creating a ConfigDirector console logger with a different log level, or by implementing the ConfigDirectorLogger interface to provide your own logger. The interface can be used to create an adapter to another logging library.

Configure the ConfigDirector console logger to a different level:

config-director-setup.ts
import { createClient, createConsoleLogger } from "@configdirector/server-sdk";

export const client = createClient("YOUR-SERVER-SDK-KEY", {
  logger: createConsoleLogger("debug"),
});

await client.initialize();

Implement your own logger adapter:

config-director-setup.ts
import { createClient, ConfigDirectorLogger } from "@configdirector/server-sdk";

const myLogger: ConfigDirectorLogger = {
  debug: function (message: string, ...args: any): void {
    // your specific logging library implementation here
  },
  info: function (message: string, ...args: any): void {
    // your specific logging library implementation here
  },
  warn: function (message: string, ...args: any): void {
    // your specific logging library implementation here
  },
  error: function (message: string, ...args: any): void {
    // your specific logging library implementation here
  },
};

export const client = createClient("YOUR-SERVER-SDK-KEY", {
  logger: myLogger,
});

await client.initialize();

connection

The connection object accepts four optional values:

  • mode
    • The connection mode, which can be either streaming or polling. It is recommended to use the default of streaming unless you have a specific need to use polling instead.
    • Defaults to streaming
  • pollingInterval
    • The interval, in seconds, between requests for config updates when mode is polling. It has no effect in streaming mode. A value below the minimum of 60 seconds is raised to 60 seconds and a warning is logged. See Polling intervals.
    • Defaults to 300 seconds
  • timeout
    • The timeout, in milliseconds, to be used in initialization. This is how long the initialize method will wait for data from ConfigDirector services before resolving its Promise. If the timeout is reached, initialize will return but the client will still be in an unready status and returning default values. The client will continue to attempt to connect and retrieve config values in the background.
    • Defaults to 3000 milliseconds
  • url
    • The base URL used to connect to ConfigDirector services
    • This should only be provided if your environment requires you to configure a proxy server in order to connect to ConfigDirector services
config-director-setup.ts
import { createClient } from "@configdirector/server-sdk";

export const client = createClient("YOUR-SERVER-SDK-KEY", {
  connection: {
    mode: "streaming",
    timeout: 2_000, // 2,000 milliseconds initialization timeout
  },
});

await client.initialize();

telemetry

Telemetry tuning. It is unlikely these settings need to be adjusted. However, in cases where your application has a large number of evaluations per second, you can adjust these settings to tune the memory footprint and frequency of telemetry requests.

Keep in mind that ConfigDirector relies on these telemetry events to provide insights and features related to the configs being used.

The telemetry object accepts the following optional values:

  • eventQueueLimit
    • The size limit of telemetry event queues. If the size limit is reached before the events are flushed to the network, older events will be dropped.
    • ConfigDirector keeps a count of dropped events. If the number of dropped events is higher than 50% of the total events, ConfigDirector will issue an alert in the dashboard.
    • A number between 100 and 100,000. Defaults to 5,000.
  • flushInterval
    • How often events are flushed and sent over the network in milliseconds.
    • Decrease this number if your application consistently captures a large number of events in short periods of time in order to reduce memory footprint from a large event queue.
    • Defaults to 30,000 milliseconds (30 seconds).
config-director-setup.ts
import { createClient } from "@configdirector/server-sdk";

export const client = createClient("YOUR-SERVER-SDK-KEY", {
  telemetry: {
    eventQueueLimit: 5_000,
    flushInterval: 30_000,
  },
});

await client.initialize();

Hooks

Hooks are events you can subscribe to in order to be notified of some key actions from the client. In order to subscribe to hooks you can provide a handler (or an array of handlers) in the options:

config-director-setup.ts
import { createClient } from "@configdirector/server-sdk";

export const client = createClient("YOUR-SERVER-SDK-KEY", {
  hooks: {
    configEvaluated: (event) => { console.log("Received configEvaluated:", event); },
  },
});

await client.initialize();

The following hooks are available:

  • clientReady
    • Emitted when the client is initialized. When this event is emitted it means the client has received a payload from the ConfigDirector servers and is ready to evaluate configs.
  • configsUpdated: { keys: string }
    • Emitted when a payload is received from the ConfigDirector servers with config data. It is emitted during initialization when an entire config payload is received. After initialization, it is emitted when updates are pushed from the server (or discovered via polling if that connection mode is used).
    • keys - A string array listing the config keys that were included in the payload from the server, and the keys of configs whose targeting rules use a segment the payload included.
    • removedKeys - A string array listing the keys of configs the payload removed. A config is removed when a full payload no longer includes it. Empty when nothing was removed.
  • configEvaluated: { evaluation: ConfigEvaluation }
    • Emitted whenever a config is evaluated. This includes calls to getValue and evaluations delivered to listeners via watch.
    • evaluation - A ConfigEvaluation object containing the details of the evaluation:
      • key: string - The config key that was evaluated
      • value: string | number | boolean | object | ConfigEnumLikeType - The value the config evaluated to. It can be the default value provided to getValue or watch. For example, if the config was evaluated before the client was initialized.
      • valueId: string | undefined - The value ID, which is a stable hash of the value that can be used for analytics or other third parties rather than sending the value. This can be useful for large values, like a JSON config, or to avoid disclosing the values themselves to third parties. It may be undefined if the evaluation had to fall back to the default value.
      • isDefaultValue: boolean - Whether or not the evaluation fell back to the default value provided in code.
      • reason: EvaluationReason - The reason for the evaluation resolution. In the case the config successfully evaluated based on server-provided targeting rules, it will be found-match. If the evaluation had to fall back to the default value, the reason will encode why the fallback was required:
        • client-not-ready - The evaluation happened before the client finished initialization
        • config-state-missing - The requested config key was not present in the payload received from the server. This could be due to an incorrect config key, or a config that was not enabled to be available to client SDKs.
        • value-missing - The config key was present in the payload received from the server, but it carried no value or an empty string.
        • invalid-number - The config was requested as a number but the value received from the server could not be converted to a number.
        • invalid-boolean - The config was requested as a boolean but the value received from the server could not be converted to a boolean.
        • invalid-json - The config was requested as a JSON object but the value received from the server could not be converted to a JSON object.
        • type-mismatch - The config was requested with a data type that did not match the type of the config and no reasonable type conversion was possible.
      • context: ConfigDirectorContext | undefined - The user context that was provided to the evaluation function, or undefined if no context was provided.

Retrieve config values

Retrieving config values via getValue, and subscribe to updates via watch:

main.ts
import { client } from "./config-director-setup"

// Retrieve the current value
client.getValue("my-config-key", false);

// Subscribe to value updates
const unwatchMyKey = client.watch("my-config-key", false, (newValue) => {
  console.log("Value updated:", newValue);
});

unwatchMyKey(); // Call the unwatch function returned to remove the listener

client.unwatch("my-config-key"); // Removes all listeners for that key

Evaluate config values with a user context

getValue accepts three arguments, the first argument is the config key, the second one is the default value to be returned if the client is not yet initialized, and the third and optional argument is a user context to be used for targeting rules evaluation:

main.ts
client.getValue("my-string-config-key", "Default");

client.getValue("my-boolean-config-key", false, { id: "user-id", name: "Example User" });

client.getValue<MyEnum>("my-enum-config-key", MyEnum.SomeDefaultValue, {
  id: "user-id",
  traits: { region: "Australia" },
});

watch accepts four arguments, the first is the config key, the second argument is the default value, the third argument is a callback function that will be executed when the config value is updated or with the default value when the config is removed, and the fourth and optional argument is a user context:

main.ts
const unwatchMyKey = client.watch(
  "my-string-config-key",
  "Default",
  (newValue) => {
    console.log("Value updated:", newValue);
  },
  { id: "user-id", name: "Example User", traits: { region: "North America" } }, // User context
);

unwatchMyKey(); // Call the unwatch function returned to remove the observer

Other useful client features

The client provides additional properties and methods.

isReady

Returns a boolean indicating if the client has been successfully initialized and is ready to evaluate configs. It is initially false and becomes true after initialize succeeds.

unwatchAll

Removes all listeners for all config keys that were previously created via watch.

dispose

Removes all listeners and observers, and closes all connections to ConfigDirector services. Only call dispose when your application shuts down and it will no longer make use of the client instance.

It is generally not needed for applications to call dispose explicitly.

Test your code

Use the SDK's testing entry point, @configdirector/server-sdk/testing, to test the code that reads your configs and flags. It creates a test client: the SDK's real client connected to an in-memory server that your test controls. Only the connection to ConfigDirector is replaced. Everything else is the same code that runs in production, so the code under test behaves exactly as it does against ConfigDirector, and no network connection is opened and no telemetry is sent.

import { createTestClient } from "@configdirector/server-sdk/testing";

const testClient = createTestClient({ values: { "new-checkout": true, "max-items": 20 } });
await testClient.client.initialize();

const service = new CheckoutService(testClient.client);
expect(service.isNewCheckoutEnabled({ id: "user-123" })).toBe(true);

testClient.setValue("new-checkout", false);
expect(service.isNewCheckoutEnabled({ id: "user-123" })).toBe(false);

testClient.client is a ConfigDirectorClient, so it goes anywhere your code accepts one. It starts uninitialized, like a production client, because the code under test usually owns the call to initialize. Code that creates the client when a module loads is tested by swapping that module's client for testClient.client with your test runner's module mocking, or by passing the client in through your own dependency injection.

Values

values, setValue, and replaceValues take native values, and the config type follows from each value: a boolean, an integral number (integer), any other number (float), a string, or a plain object or array (json). Every value is served as an unconditional config, so every context receives the same value; a test that needs different values per context is written as one test per value. A null or undefined value, a non-finite number, an integer beyond Number.isSafeInteger, an empty key, or JSON contents that cannot be encoded (functions, Date or Map instances, bigint) throw a ConfigDirectorValidationError, which the entry point exports.

Reads behave as they do in production: setValue("k", true) read as a string returns the in-code default value with the type-mismatch reason, and setValue("k", "") serves the in-code default value with the value-missing reason.

Controls

ControlEffect
setValue(key, value)Stores the value and, once the client is connected, delivers an update carrying only key. Watchers of key, configsUpdated handlers, and reads see it at once.
removeValue(key)Removes the value and delivers a full update without it. Reads return the in-code default value with the config-state-missing reason, watchers of key receive the default, and configsUpdated lists key in removedKeys.
replaceValues(values)Replaces every stored value, disarms any armed hold or failure, and delivers a full update. Use it to reset a test client shared across tests.
holdInitialization()The next initialize waits until completeInitialization() or failInitialization(), or until the client's timeout elapses.
completeInitialization()Delivers the stored values to the held initialize, which completes with the client ready. Called while a hold is armed but not picked up, it disarms the hold.
failInitialization()Fails the held initialize the way an invalid SDK key does: it completes promptly, the client is not ready, connectionError fires, and an error is logged. Called with no initialize held, it arms the next one to fail.

After a failed attempt, the next initialize succeeds and delivers the values stored in the meantime.

const testClient = createTestClient({ values: { "new-checkout": true }, timeout: 500 });
testClient.failInitialization();

await testClient.client.initialize();
expect(testClient.client.isReady).toBe(false);
expect(service.isNewCheckoutEnabled({ id: "user-123" })).toBe(false);

Options

createTestClient takes values, timeout (the client's connection timeout in milliseconds, which bounds how long a held initialize waits; defaults to the SDK's production timeout), and logger.

What to expect

  • Under the test client, the first update is delivered while initialize connects, so watchers registered before initialize run before it resolves, and configsUpdated fires before clientReady.
  • A held initialize that times out, or that is interrupted by dispose, leaves the client not ready until the next initialize; production streaming would keep retrying in the background. Close the client with dispose, which ends a held initialize; closeConnection does not.
  • Watchers fire on every update carrying their key, whether or not the value changed, as in production. removeValue and replaceValues deliver a full update, so they also fire the watchers of every remaining key.
  • An exception thrown by a watcher during a delivery is logged and does not propagate, as with the production transports.
  • The configEvaluated event is emitted from a setTimeout(0). With fake timers, advance them to receive it.
  • After dispose, the test client's controls are silent no-ops, and nothing from the SDK is left running.