Skip to main content
The Foff TypeScript SDK integrates feature flag resolution into any Node.js application. It polls the Foff API in the background, caches configs in memory, and lets you resolve the right flag value for any hierarchy with a single synchronous call. The package ships with full TypeScript types.

Requirements

Node.js 18 or later.

Installation

npm install @twospoon/foff-feature-config-typescript-sdk

Quick start

import { Client, Config } from "@twospoon/foff-feature-config-typescript-sdk";

// STEP 1: Create appropriate config
const config = new Config({
  APIKey: process.env.FOFF_API_KEY!,
  BaseURL: "https://foff.twospoon.ai/live",
  Scope: "name-of-your-scope",
  PollingInterval: 30, // refresh configs every 30 seconds
});

// STEP 2: Create the client with the config
const client = await Client.newClient(config);

// STEP 3: Retrieve configs for your created features for given hierarchies
const value = client.getFeatureConfig("my-feature", ["org-1", "team-a", "user-123"]);
console.log(value);

// On shutdown
client.close();
Store your API key in an environment variable such as FOFF_API_KEY and read it with process.env.FOFF_API_KEY. Never hard-code it in source files.

Configuration

Create a Config instance with the fields below and pass it to Client.newClient.
FieldTypeRequiredDefaultDescription
APIKeystringYesYour Foff API key.
BaseURLstringYesBase URL of the Foff API. Use https://foff.twospoon.ai/live.
ScopestringYesThe scope to fetch configs for, such as production or staging.
PollingIntervalnumberNo30How often, in seconds, to poll for config updates. Use 0 to disable polling.

Validation

Client.newClient validates the config before creating the client. It throws an error if APIKey, BaseURL, or Scope is missing or empty.

Polling interval normalization

The polling interval is clamped to safe bounds automatically before the client starts.
If you set PollingInterval below 10 seconds, the SDK raises it to 10. If you set it above 3600 seconds (1 hour), the SDK lowers it to 3600. Set it to 0 to disable polling entirely.

Creating a client

const client = await Client.newClient(config);
Client.newClient performs the following steps in order:
1

Validate the config

Checks that APIKey, BaseURL, and Scope are non-empty. Throws if any required field is missing.
2

Normalize the polling interval

Clamps PollingInterval to the range 10–3600 seconds (unless it is 0).
3

Fetch all configs (blocking)

Makes an HTTP GET request to {BaseURL}/api/v1/scopes/{scope}/configs and populates the in-memory cache.
4

Start background polling

If PollingInterval > 0, starts a timer that re-fetches configs on the configured interval.
The initial fetch is blocking. If the Foff API is unreachable or returns an error at startup, newClient throws that error immediately. Handle it at startup so your process fails fast rather than serving stale or empty configs.

Resolving feature configs

getFeatureConfig(featureName, orderedHierarchy)

getFeatureConfig(featureName: string, orderedHierarchy: string[]): unknown
Returns the config value for a feature, resolved against a hierarchy. The call is synchronous and reads from the in-memory cache — no network request is made.
const value = client.getFeatureConfig("dark-mode", ["org-1", "team-a", "user-123"]);

Hierarchy resolution algorithm

The SDK resolves from most-specific to least-specific:
  1. It checks the full combination first — org-1 + team-a + user-123.
  2. It then tries shorter prefixes — org-1 + team-a, then org-1.
  3. If no override matches, it returns the feature’s "default" value.
  4. If the feature does not exist at all, it returns null.
This means you can define overrides at any level of your hierarchy — organisation, team, user, environment, region, service — and the SDK finds the most specific match automatically.

Polling

When PollingInterval is greater than 0, the SDK polls GET {BaseURL}/api/v1/scopes/{scope}/configs in the background and replaces the in-memory cache on each successful response.
  • Polling errors are silently ignored. The SDK continues serving the last successfully fetched config.
  • Calling client.close() stops the background timer and releases all resources.
Use caseInterval
Near-real-time10s
Standard30–60s
Low-traffic / batch300–600s

Client lifecycle

For short-lived scripts, use a try/finally block to guarantee the client is closed:
const client = await Client.newClient(config);

try {
  const value = client.getFeatureConfig("dark-mode", ["org-1", "team-a", "user-123"]);
  console.log(value);
} finally {
  // Always close to stop background polling
  client.close();
}
For long-lived processes such as an Express server, create the client once at startup and close it on process exit:
const client = await Client.newClient(config);

process.on("SIGTERM", () => {
  client.close();
  process.exit(0);
});