OpenFeature
ShipSilently implements the OpenFeature provider interface, so your evaluation call sites stay vendor-neutral and switching providers is a one-line change.
Install
The provider lives on a subpath of the Edge SDK.
@openfeature/server-sdk is an optional peer dependency,
you only need it if you import this subpath; the core SDK has no OpenFeature
dependency at all.
npm install @shipsilently/edge @openfeature/server-sdk Register the provider
// ShipSilently as an OpenFeature provider. Call sites stay vendor-neutral —
// swapping providers is a one-line change and your evaluation code never moves.
//
// `@openfeature/server-sdk` is an OPTIONAL peer dependency: install it only if
// you import this subpath. The core SDK has no OpenFeature dependency.
//
// npm install @shipsilently/edge @openfeature/server-sdk
import { OpenFeature } from '@openfeature/server-sdk';
import { ShipSilentlyProvider } from '@shipsilently/edge/openfeature';
// Accepts the same config as EdgeClient.init — hosted, kv(), or staticProvider().
const provider = new ShipSilentlyProvider({ apiKey: process.env.SHIPSILENTLY_KEY! });
// setProviderAndWait runs initialize(), which loads the flag blob.
await OpenFeature.setProviderAndWait(provider);
const client = OpenFeature.getClient();
// `targetingKey` maps to the context's `key`; other primitive attributes pass
// through untouched for targeting rules.
export const enabled = await client.getBooleanValue('new-checkout', false, {
targetingKey: 'u_123',
plan: 'pro',
});
// Evaluation details carry the mapped OpenFeature reason:
// rule_match → TARGETING_MATCH rollout → SPLIT
// flag_disabled → DISABLED flag_not_found → ERROR (FLAG_NOT_FOUND)
export const details = await client.getBooleanDetails('new-checkout', false, {
targetingKey: 'u_123',
}); setProviderAndWait, not setProvider.
The provider's initialize() is what loads the flag blob. Resolve
a flag before it finishes and you get your default, silently.
Context mapping
OpenFeature's EvaluationContext maps onto ShipSilently's
attribute model like this:
| OpenFeature | ShipSilently |
|---|---|
targetingKey | key |
| String / number / boolean attributes | Passed through unchanged |
| Nested objects and arrays | Dropped — not part of the attribute model |
targetingKey is not userId. It maps
to key, while rollout bucketing reads userId (then
email). If you use percentage rollouts through OpenFeature, set
userId explicitly in the context as well, otherwise every user
buckets identically.
Reason mapping
| ShipSilently reason | OpenFeature reason |
|---|---|
rule_match | TARGETING_MATCH |
rollout | SPLIT |
flag_disabled | DISABLED |
default | DEFAULT |
flag_not_found | ERROR with errorCode: FLAG_NOT_FOUND |
When a rule decided the result, its UUID is surfaced as the OpenFeature
variant.
Wrapping an existing client
If you already hold an EdgeClient, wrap it instead of building a
second one, so both share a single blob and a single refresh loop:
import { EdgeClient } from '@shipsilently/edge';
import { ShipSilentlyProvider } from '@shipsilently/edge/openfeature';
const client = EdgeClient.init({ apiKey: process.env.SHIPSILENTLY_KEY! });
await client.load();
const provider = ShipSilentlyProvider.fromClient(client); Performance
The provider's resolve* methods are async because
the OpenFeature interface requires it, but there is no I/O on the resolve
path once the blob is loaded. Every resolution is the same synchronous,
sub-millisecond in-memory evaluation the Edge SDK
performs directly.