Skip to main content
darkship docs
Open console

SDKs · JavaScript / TypeScript

JavaScript SDK

One package, two modes. In Node it evaluates locally from an in-memory ruleset. In the browser it asks the evaluation service and caches the answers for the session. The API shape is the same.

Updated Oct 28, 20266 min read Edit this page on GitHub (opens in a new tab)

@darkship/sdknpm package, ESM + CJS, bundled types
0.1.0pre-1.0: minor versions may break
Node 18+server mode, local evaluation
ES2020browser mode, remote evaluation

Install#

Shellterminal
$ npm install @darkship/sdk
$ pnpm add @darkship/sdk
$ yarn add @darkship/sdk

Initialise#

init() returns a client immediately and loads flags in the background. The key prefix selects the mode: ds_srv_ runs server mode, ds_cli_ runs client mode. The browser build refuses a ds_srv_ key at init.

TypeScriptdarkship.ts
import { init } from '@darkship/sdk';

export const ds = init({
  sdkKey: process.env.DARKSHIP_SDK_KEY!,
  // host: DARKSHIP_BASE_URL or baseUrl; changes arrive over SSE within seconds (polling is the fallback)
});

ds.on('error', (err) => logger.warn({ err }, 'darkship error'));
await ds.waitForReady({ timeout: 3000 });

Init options#

Init options. Every request carries Authorization: Bearer <key> and Darkship-Sdk: js/<version>.
OptionTypeDefaultDescription
sdkKeystringrequiredds_srv_... or ds_cli_.... Determines mode and environment.
contextContextundefinedClient mode only: initial evaluation context. Change it with identify().
baseUrlstringDARKSHIP_BASE_URL, else https://sdk.darkship.techEvaluation host. Override for a proxy or a self-hosted test double. https only; plain http is accepted for loopback hosts.
allowInsecureHttpbooleanfalseAllow plain http on a non-loopback host (a private network or a test double). The SDK emits a warning: the key travels unencrypted.
streamingbooleantrueServer mode. Subscribes to SSE (GET /v1/sdk/stream): a change reaches the SDK within seconds. On any stream error the SDK keeps polling and reconnects. false for polling only (for example behind a proxy that buffers responses).
pollIntervalMsnumber30000Server mode. The fallback poll while the stream is down (every 5 minutes while it is connected). Jittered by ±20%. Retry-After is honoured and failures back off exponentially.
timeoutMsnumber3000Bound on every network call. SDK work never runs on your call path.
bootstrapSnapshot or allFlags() outputundefinedServer mode: a snapshot you saved earlier (exportSnapshot()), used until the first fetch succeeds. Client mode: the output of allFlags() from your server, for server-side rendering.
persistbooleanfalseClient mode. Keeps the last resolved values (never the context) in localStorage.
fetchfunctionglobal fetchInjectable, for tests and proxies.
telemetryboolean or objecttrueServer mode. Aggregate counters only; false turns them off. See telemetry.
telemetry.flushIntervalMsnumber60000Also flushes on a size threshold and on close().
Each poll is a request

Every snapshot fetch (GET /v1/sdk/snapshot), including a 304, counts toward your plan's monthly requests. A fleet of 200 processes polling every 30 seconds is about 576,000 requests a day. With the default streaming: true a connected SDK only polls every 5 minutes as a safety net; if you turn streaming off, raise pollIntervalMs where fast propagation is not critical.

waitForReady#

await ds.waitForReady({ timeout }) resolves with true when a ruleset (or, in client mode, the first evaluation result) is available, and false on timeout. It never rejects. Evaluations before readiness return your default with reason NOT_READY.

Evaluate flags#

Each accessor takes the flag key, a context and a default in server mode: getBooleanValue(key, ctx, default). In client mode there is no context argument: getBooleanValue(key, default) uses the context set at init or by identify. They are synchronous and perform no I/O.

Evaluation methods. Server signature shown; in client mode omit ctx.
MethodAliasReturnsFlag type
getBooleanValue(key, ctx, def)boolVariationbooleanboolean
getStringValue(key, ctx, def)stringVariationstringstring
getIntValue(key, ctx, def)intVariationnumber (integer)int
getFloatValue(key, ctx, def)floatVariationnumberfloat
getJsonValue<T>(key, ctx, def)jsonVariationTjson
getDateValue(key, ctx, def)dateVariationDatedate
getNumberValue(key, ctx, def)numberint or float
get*Detail(...)*VariationDetail{ flagKey, value, variationKey, reason, ruleId?, bucket?, errorCode?, flagType? }same as above

Other methods: allFlags(ctx) (values only; usable as a client bootstrap), refresh(), isReady(), close(); server: exportSnapshot(), snapshotVersion(), flagKeys(); client: identify() (alias setContext()).

TypeScriptdetail.ts
const d = ds.getBooleanDetail('new-checkout-flow', ctx, false);
// { flagKey: 'new-checkout-flow', value: true, variationKey: 'on', reason: 'FALLTHROUGH', bucket: 4127, flagType: 'boolean' }

const bad = ds.getIntDetail('promo-banner-text', ctx, 0);
// { flagKey: 'promo-banner-text', value: 0, reason: 'ERROR', errorCode: 'TYPE_MISMATCH' }

Reasons and error codes#

Values of reason and errorCode.
reasonMeaning
OFFEnvironment disabled; served offVariation.
TARGET_MATCHA target list matched: the context key, email, a groups element or another listed attribute.
RULE_MATCHA rule matched and served a fixed variation.
FALLTHROUGHEnabled, nothing matched; served the default variation. When a percentage rollout decided (in a rule or the fallthrough) the detail carries bucket (0 to 9999); there is no separate rollout reason.
DEFAULTYour default was returned because the client was closed.
NOT_READYNo ruleset yet; your default was returned, with errorCode PROVIDER_NOT_READY.
ERRORSomething went wrong; your default was returned. See errorCode: FLAG_NOT_FOUND, TYPE_MISMATCH, INVALID_CONTEXT, MALFORMED_FLAG, GENERAL, PROVIDER_NOT_READY.

Context#

A context is a plain object. key (the user id) is required: it is the default bucketing attribute and what key target lists match. email, name and groups are optional, conventional fields: target lists can match by email (normalised: NFC, trimmed, lowercased on both sides) or by groups (matches when any element is listed). Everything else is an attribute that rules can test. See target users by id, email or group.

TypeScriptcontext.ts
type Context = {
  key: string;                       // stable id: user id, device id, org id
  email?: string;                    // matched case-insensitively by email lists and rules
  name?: string;
  groups?: string[];                 // any element may match a groups list
  [attribute: string]: string | number | boolean | string[] | undefined;
};

const ctx = {
  key: 'usr_marta',
  email: 'Marta@Acme.com',
  groups: ['beta-testers', 'acme-staff'],
  plan: 'pro', country: 'BR', app_version: '4.12.0', orgId: 'org_acme',
};
  • Use a stable key. A random key per request defeats sticky rollouts.
  • Don't send secrets or personal data you don't need for targeting. In server mode the context never leaves your process; in client mode it is sent for evaluation, used in memory and discarded.
  • Browser: await ds.identify(newContext) replaces the context, triggers one re-fetch and emits update if any value changed.

Events#

TypeScriptevents.ts
ds.on('ready',  ({ version }) => log.info('ruleset', version));
ds.on('update', ({ version, changed }) => log.info('flags changed', changed));
ds.on('error',  (err) => log.warn(err.code, err.message));   // never thrown, only emitted
ds.on('stale', () => log.warn('serving the last good ruleset'));
ds.on('recovered', () => log.info('control plane reachable again'));

Telemetry and privacy#

The SDK counts evaluations in memory per flag × variation × one-minute window and flushes the counters to POST /v1/telemetry/counters on an interval, when a size threshold is reached, and on close(). These are the only fields that leave your process:

Counter payload.
FieldExampleNotes
environmentderived from keyNever sent as free text.
flag_keycheckout-v2
variationonThe variation name, not the value of a JSON flag.
window_start2026-10-28T14:03:00ZOne-minute window.
count1842
sdk, sdk_versionjs/0.1.0For support and deprecation planning.
By design

No context, no user id, no per-user history and no raw events are stored. The console shows distributions and counts. If you need "who saw what", log it in your own system.

Error handling and defaults#

Evaluation never throws. When anything is wrong you get the default you passed, plus a reason in the *Detail variants and an error event for connectivity problems.

What you get, and why.
SituationReturnedReason / code
Called before the first rulesetyour defaultNOT_READY / PROVIDER_NOT_READY
Flag key doesn't exist in this environmentyour defaultERROR / FLAG_NOT_FOUND
Accessor type differs from the flag typeyour defaultERROR / TYPE_MISMATCH
Network failure after a ruleset was loadedevaluates from the last rulesetunchanged; error event emitted
Invalid context (for example no key)your defaultERROR / INVALID_CONTEXT
Internal erroryour defaultERROR / GENERAL

init() itself throws synchronously only for programmer errors: no key, a malformed prefix, or a server key in a browser build.

Shutdown#

TypeScriptshutdown.ts
process.on('SIGTERM', async () => {
  await ds.close({ timeout: 2000 });   // stop polling, flush counters (best effort), release timers
  process.exit(0);
});

In serverless functions call await ds.close() at the end of the invocation, or keep the client at module scope across warm invocations and flush before the runtime freezes.

Versions and support#

The package is at 0.1.0. Until 1.0, minor versions may include breaking changes, listed in the changelog. Server mode needs Node 18 or newer; client mode runs in evergreen browsers. Behaviour is verified against the shared conformance vectors through both the local and the remote path, and a change to targeting semantics ships in the same release as the eval service change.

Constructing the SDK with a ds_srv_ key in a browser, web worker or jsdom throws DarkshipConfigError("SERVER_KEY_IN_BROWSER").

Was this helpful?