Shipeasy

Node / TypeScript

The canonical Shipeasy SDK — one package with a server and a browser build, local evaluation, configs, experiments, and tracking.

Production readyOn this page · 8 min readUpdated · June 18, 2026Works with · Node · Cloudflare Workers · Bun · Deno · Next.js

@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

$npm install @shipeasy/sdk

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 }),
});
Server key, server only

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-found

For 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" });
MethodPolls in backgroundUse when
init()yesLong-running server, Worker, container
initOnce()no — one fetchLambda / 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 events

Testing

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.

Was this page helpful?
✎ Edit this page

On this page