Node / TypeScript
The canonical Shipeasy SDK — one package with a server and a browser build, local evaluation, configs, experiments, and tracking.
@shipeasy/sdk is the reference implementation. It ships conditional exports — @shipeasy/sdk/server (Node, Workers, Bun, Deno, Next.js Server Components) and @shipeasy/sdk/client (browser). This page covers the server build; for the browser see Browser & React.
Install
Configure
Initialise once, with the server key, at the entry point that runs per cold start — the Next.js root layout.tsx, an Express initialiser, or your Worker fetch handler. configure() is idempotent.
Pass an optional attributes transform that maps your user object onto the Shipeasy attribute map — declare it once and every bound client reuses it.
import { configure, Client } from "@shipeasy/sdk/server";
configure({
apiKey: process.env.SHIPEASY_SERVER_KEY ?? "",
attributes: (u) => ({ user_id: u.id, plan: u.plan, country: u.country }),
});Never pass clientKey to /server or serverKey to
/client. The server key reads full payloads and writes events — it must not land in a
browser bundle. See Keys & environments.
After configure(), reads are synchronous — the rule set lives in process memory and is refreshed by a background poll.
Bind a client to the user
Construct a lightweight Client from your user object — the attributes transform turns it into the evaluation context, so the getters take no user argument:
const flags = new Client(currentUser);The Client is cheap to construct per request; the heavy polling state lives in the shared engine behind configure().
Evaluate a feature flag
const enabled = flags.getFlag("new_checkout");A caller-supplied default is returned only when the value can't be evaluated — the client isn't ready or the gate isn't in the blob. A gate that evaluates false (disabled, denied, or rolled out to 0%) returns false, not the default.
flags.getFlag("new_checkout"); // false for a missing flag
flags.getFlag("new_checkout", true); // true only if not-ready / not-foundFor the resolved value and the reason, use getFlagDetail — see Evaluation reasons:
import type { FlagReason } from "@shipeasy/sdk/server";
const d = flags.getFlagDetail("new_checkout");
// → { value: true, reason: "RULE_MATCH" }Read a dynamic config
const pricing = flags.getConfig<{ base: number; currency: string }>("pricing", {
base: 9.99,
currency: "USD",
});
const base = pricing?.base ?? 9.99;Configs are typed via the optional generic. The default is returned when the key is absent.
Resolve an experiment
const result = flags.getExperiment<{ color: "blue" | "green" }>(
"checkout_button",
{ color: "blue" }, // defaultParams when not in experiment
);
if (result.inExperiment) {
// result.group e.g. "control" | "treatment"
// result.params.color
}Assignment is deterministic by user_id (resolved from the bound user). The first call for a user queues an exposure event for the daily analysis pipeline.
Track an event
flags.track("purchase", { value: 49.99, sku: "SHIRT-L-BLUE" });track() is fire-and-forget — events are batched and sent to /collect on the Shipeasy edge.
The low-level per-call API
The old per-call style still works for back-compat — you can construct an Engine directly and pass the user on every call (getFlag(name, user)). This is what new Client(user) is built on; reach for it only when you need the engine itself (see init vs initOnce below):
import { Engine } from "@shipeasy/sdk/server";
const engine = new Engine({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
await engine.init();
engine.getFlag("new_checkout", { user_id: "u1", plan: "pro" });init vs initOnce (serverless)
configure() starts a background poll thread, which is what you want in a long-running process. In a short-lived function (Lambda, Cloud Run cold start) you don't want a poll loop — construct an Engine directly and do a single synchronous fetch with initOnce():
import { Engine } from "@shipeasy/sdk/server";
const engine = new Engine({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
await engine.initOnce(); // one fetch, no background thread
engine.getFlag("new_checkout", { user_id: "u1" });| Method | Polls in background | Use when |
|---|---|---|
init() | yes | Long-running server, Worker, container |
initOnce() | no — one fetch | Lambda / serverless / scripts |
React to rule changes
The engine fires registered listeners after a background poll returns new data (HTTP 200, not 304). Returns an unsubscribe function; never fires in test/offline mode. See onChange.
import { Engine } from "@shipeasy/sdk/server";
const engine = new Engine({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
await engine.init();
const unsubscribe = engine.onChange(() => {
console.log("flag rules changed — re-evaluating");
});
// later…
unsubscribe();Shut down cleanly
In a long-running worker you control, stop the poll thread on shutdown:
engine.destroy(); // stops polling, flushes pending eventsTesting
Build a no-network Engine with forTesting() and seed overrides — no key, no network, telemetry off, init/track are no-ops. See Testing.
import { Engine } from "@shipeasy/sdk/server";
const engine = Engine.forTesting();
engine.overrideFlag("new_checkout", true);
engine.getFlag("new_checkout", { user_id: "u1" }); // true
engine.clearOverrides();Errors & feedback
Report a handled exception with see() so it folds into the errors primitive with a one-sentence consequence — what feature broke and how it degraded. see() rides the same configure() boot (server key); there is no separate error SDK or second key.
import { see } from "@shipeasy/sdk/server";
try {
await chargeCard(order, prices);
} catch (e) {
see(e).causes_the("checkout").to("use cached prices").extras({ order_id: order.id });
}Use see.Violation("large query") for a non-exception problem, and see.ControlFlowException(e).because("…") for expected control flow that should report nothing. Full grammar — consequence phrasing, control-flow exceptions, anti-patterns — is in Error reporting with see().
The in-app bug & feature report overlay is a standalone <script> tag you drop into your frontend — platform-agnostic, no server SDK required. See The devtools overlay.