@westyx-nexus/openfeature-provider-nodejs package wraps a NexusClient as an OpenFeature Server SDK provider. It reads boolean flags from the Nexus flags cache and string/number/object values from the Nexus configs cache.
Requirements
@westyx-nexus/sdk-nodejs>= 0.11.0@openfeature/server-sdk>= 1.22.0- Node.js >= 22
Installation
The
@westyx-nexus/openfeature-provider-nodejs package is published to the same GitLab Package Registry as the main SDK. Ensure the @westyx-nexus scope points to the GitLab registry in your .npmrc - see Installation.Quick start
Resolution behaviour
Every resolution reads the in-memory cache, which the SDK keeps fresh through background polling and the SSE live-update stream. The one exception is a boolean evaluation whose context carries a targeting key - see Per-user targeting.
Boolean flags
Boolean evaluation is two map lookups. A missing key returns your default withFLAG_NOT_FOUND. No TYPE_MISMATCH is possible because Nexus flags are always boolean.
The existence check runs before anything else, and it is what keeps an unknown key away from a per-user evaluation: the service answers false for a key it does not know, which is indistinguishable from “exists and off”, so sending one would override your true default while reporting a successful targeted evaluation.
String configs
The stored config value must already be a string. Numbers, booleans, and objects returnTYPE_MISMATCH - use getNumberValue / getObjectValue for those.
Number configs
The stored value must be a number. Abigint a number can hold exactly is converted; one that cannot is a TYPE_MISMATCH, since returning it would undo the exactness the SDK preserved.
A type mismatch is reported rather than converted, because Number() accepts values it should not: Number(true) is 1, and Number("") and Number([]) are both 0, so a boolean or an empty string would resolve to a confident-looking zero. A numeric string such as '42' is rejected on the same principle - the config holds a string, and reinterpreting it would hide the mismatch rather than report it. Store numbers as JSON numbers.
Object configs
The config value must be a non-null object or array. Primitive values yieldTYPE_MISMATCH.
Inside the resolved structure, an integer beyond Number.MAX_SAFE_INTEGER is carried as its exact decimal string, at any depth - OpenFeature’s JsonValue has no numeric slot that holds it exactly, and the decimal string is the lossless one, so the resolved value survives JSON.stringify. Ordinary numbers are untouched; parse the string with BigInt(value) where the magnitude matters. The scalar number resolution answers TYPE_MISMATCH for the same value on purpose.
Per-user targeting
Added in v0.12.0.
EvaluationContext carries a targetingKey, a boolean evaluation is resolved per user through the Nexus AB Testing add-on: the flag’s rollout percentage and cohort rules are applied to that identity. Without a targeting key the provider reads the synced snapshot, which is a pure in-memory lookup.
Attributes are strings
Cohort conditions compare attribute values as strings (eq, neq, in), so only string context fields are sent. A non-string field is left out of the evaluation and reported through the OpenFeature logger at debug level, naming the field and its type.
The other resolutions are never targeted
String, number and object values come from configs, and a config has no per-user dimension in Nexus. OnlygetBooleanValue can be targeted.
Requests and caching
A targeted evaluation is a network call, where the snapshot path is a map read. Two things keep that affordable, and neither needs configuring:- One request serves every flag the snapshot knows for that user, so reading five flags while handling a request costs one round trip, not five. A snapshot larger than the service’s 200-key limit is split into successive calls.
- Results are memoised per user and flag for 30 seconds, and concurrent evaluations for the same user coalesce into a single request. A memo hit reports the
CACHEDreason.
Options
Each option is validated at construction, naming the option and the value, so an unusable window surfaces where it is set instead of turning into unexplained load later.
targetingTtlMs: 0 is rejected rather than read as “no caching” - it would make every evaluation a network call.
The provider needs no logger option: the OpenFeature Node.js SDK passes a Logger into every resolution, and that is where the provider writes its diagnostics.
Resolution reasons
variant carries the string form of the resolved value for boolean, string and number resolutions, which is the convention across the Westyx Nexus SDK suite. A composite value has no variant.
A failed targeted request is not an error
The OpenFeature specification requires that evaluation not throw, and that an abnormal execution return the caller’s default. Reporting an error for a failed request would therefore hand back your default for a flag that is genuinely on - so instead the snapshot value is served with theSTALE reason: real, but not freshly targeted for this user. Per the specification, no error message is populated, because a STALE fallback is a normal execution.
A project without the AB Testing add-on gets STATIC from the snapshot, which is authoritative for it, and the provider waits five minutes before asking again. That pause is a throttle, not a latch: it expires on its own and any success clears it, so buying the add-on takes effect without a restart.
Percentage rollouts report TARGETING_MATCH rather than SPLIT. The evaluation endpoint returns the resolved booleans, so the provider cannot distinguish a cohort match from a bucketing result; the specification defines TARGETING_MATCH as the result of a dynamic evaluation such as a rule or user targeting, which covers both.
Lifecycle
TheNexusProvider does not manage the NexusClient lifecycle. Create the client before passing it to NexusProvider, and call client.close() during shutdown as usual.
Migration note (v0.8.0)
getConfig() return type changed from string | undefined to unknown. If you previously relied on the (incorrect) string type, add an explicit as string cast or use typeof value === 'string' narrowing.