Installation & configuration
One npm package, two entrypoints — @shipeasy/sdk/server (Node / Cloudflare Worker / Deno, server key) and @shipeasy/sdk/client (browser, public client key).…
/docs/ — also served raw at https://shipeasy-ai.github.io/sdk-ts/pages/installation.md.One npm package, two entrypoints — @shipeasy/sdk/server (Node / Cloudflare
Worker / Deno, server key) and @shipeasy/sdk/client (browser, public
client key). This page is the canonical home for configure(): install
- minimal wiring for the main TypeScript/JS frameworks, plus the one place each
key, the
attributestransform, the identity default, and init-vs-one-shot live.
Install
npm install @shipeasy/sdk
# or
pnpm add @shipeasy/sdk
# or
yarn add @shipeasy/sdkOne runtime dependency (murmurhash-js) and no peer dependencies at all —
installing this package never makes your package manager resolve, or
version-check, a framework you aren't using. Everything that needs a peer is its
own package:
| Package | Peers | For |
|---|---|---|
@shipeasy/next | next | edge middleware that mints the __se_anon_id bucketing cookie |
@shipeasy/openfeature | @openfeature/server-sdk / -web | OpenFeature providers (/server, /web) |
@shipeasy/react-native-devtools | react, react-native, expo-*, … | the on-device devtools overlay |
Runtime requirements
- Node ≥ 18 (server build; also runs on Cloudflare Workers and Deno).
- Any evergreen browser for the
/clientbuild. - TypeScript ≥ 5 recommended (full types ship with the package).
Entrypoints / import lines
// Server (Node / Cloudflare Worker / Deno) — uses the SERVER key
import { configure, Client, see } from "@shipeasy/sdk/server";
// Browser — uses the public CLIENT key
import { configure, Client, see, i18n } from "@shipeasy/sdk/client";
// Next.js App Router SSR bootstrap handle (server entry)
import { shipeasy } from "@shipeasy/sdk/server";
// OpenFeature providers (optional peer deps)
import { ShipeasyProvider } from "@shipeasy/openfeature/server";
import { ShipeasyProvider } from "@shipeasy/openfeature/web";One key per entrypoint. The server entry takes the server key (
configure({ apiKey })/shipeasy({ serverKey })); the browser entry takes the public client key (configure({ clientKey })). Never passclientKeyto the server entry or the server key to the browser entry.
configure() — the front door
Call configure() once at app boot, then evaluate per user with
new Client(user). configure() builds the process-wide machinery (HTTP + blob
cache + poll lifecycle) and registers your attributes transform. The first
call wins (later calls are no-ops).
| Option | Side | Default | Purpose |
|---|---|---|---|
apiKey | server | — | server key (required on @shipeasy/sdk/server) |
clientKey | browser | — | public client key (required on @shipeasy/sdk/client) |
attributes | both | identity | (yourUser) => ({ user_id, anonymous_id?, ...targeting }) — runs once per new Client(user). Omit ⇒ identity transform (you must pass the attribute bag verbatim). |
poll | server | false | true ⇒ start the background poll (initial fetch + periodic refresh) so rules stay fresh on a long-running server. The poll lifecycle lives inside the SDK — you never call an init method yourself. |
init | server | true | One-shot fire-and-forget fetch on configure() (serverless-friendly). Ignored when poll: true (the poll does the initial fetch). Set false only to control the first fetch yourself. |
baseUrl | both | https://cdn.shipeasy.ai | override the CDN/edge base |
env | server | prod | which published env to read (dev / staging / prod) |
disableTelemetry | both | false | turn off per-evaluation usage beacons. On Cloudflare Workers each beacon is an outbound subrequest (cap 50 free / 1000 paid per invocation), so set true on hot paths that evaluate many flags per request. |
privateAttributes | both | [] | attribute names usable for targeting but stripped from every outbound track() payload (LD/Statsig privateAttributes). |
stickyStore | server | — | a sticky-bucketing store so an enrolled unit stays in its first-assigned variant across requests even when allocation changes — see Advanced → sticky bucketing. |
configure() is first-config-wins: the first call builds the process-wide
state, later calls are no-ops. The test/offline siblings
configureForTesting() / configureForOffline() (see Testing)
replace it so a suite can reconfigure between cases.
Identity / bucketing unit. Bucketing hashes on user_id, falling back to
anonymous_id. To bucket a whole org together, the experiment/gate carries a
bucketBy (e.g. company_id) — see Advanced.
Env vars (convention). SHIPEASY_SERVER_KEY (server, configure({ apiKey })
/ shipeasy({ serverKey })) and NEXT_PUBLIC_SHIPEASY_CLIENT_KEY (browser,
configure({ clientKey })).
Next.js (App Router)
Next.js spans both sides: the server evaluates in Server Components / Route
Handlers, and the browser SDK reads on the client. Two wiring pieces — plus
an optional third, the edge middleware in @shipeasy/next:
npm install @shipeasy/next # optional — see "3. Middleware" below1. Root layout — SSR bootstrap (server key)
shipeasy({ serverKey }) pre-evaluates flags/configs/experiments and emits two
declarative <script> tags so the browser SDK reads them synchronously on
first paint. No SDK key is embedded in the bootstrap tag.
// app/layout.tsx — React Server Component
import { shipeasy } from "@shipeasy/sdk/server";
export default async function RootLayout({ children }) {
// Every tag value is configured once, here — the emit calls take no arguments.
const se = await shipeasy({
serverKey: process.env.SHIPEASY_SERVER_KEY ?? "",
clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY, // PUBLIC key, for the tags
projectId: process.env.NEXT_PUBLIC_SHIPEASY_PROJECT_ID, // for the devtools tag
});
const boot = se.getBootstrapData();
return (
<html>
<body>
{/* Render REAL <script> elements — dangerouslySetInnerHTML scripts do NOT run. */}
<script src={boot.bootstrap.src} {...boot.bootstrap.attrs} />
{boot.i18nLoader && <script src={boot.i18nLoader.src} {...boot.i18nLoader.attrs} />}
{children}
</body>
</html>
);
}For server-side reads in a Server Component / Route Handler, also configure()
the server engine once and bind per request:
import { configure, Client } from "@shipeasy/sdk/server";
configure({
apiKey: process.env.SHIPEASY_SERVER_KEY!, // SERVER key
attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan, country: u.geo.country }),
});
const flags = new Client(currentUser);
if (flags.getFlag("new_checkout")) { /* ... */ }2. Browser entry — "use client" (client key)
Configure the browser SDK once at startup (e.g. a client component mounted in the root layout):
"use client";
import { useEffect } from "react";
import { configure } from "@shipeasy/sdk/client";
export function ShipeasyClient() {
useEffect(() => {
configure({
clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY!, // public CLIENT key
attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan }),
});
}, []);
return null;
}Then in any "use client" component:
"use client";
import { Client } from "@shipeasy/sdk/client";
const flags = new Client(currentUser); // construct once per visitor
await flags.ready(); // optional — await first /sdk/evaluate
if (flags.getFlag("new_checkout")) { /* ... */ }React needs no wrapper package: the API is framework-agnostic, so a component reads a flag with the same
new Client(user)a route handler uses. See Browser / React below.
3. Middleware — mint the bucketing cookie at the edge (optional)
@shipeasy/next mints the shared __se_anon_id cookie before render, so the
very first request already has a stable bucketing unit — SSR and the browser
then evaluate against the identical value at any rollout percentage. Without it
the first render mints one and the bootstrap script persists it, which is one
paint later:
// middleware.ts
export { middleware, config } from "@shipeasy/next";To keep your own middleware, compose instead:
import { withShipeasy } from "@shipeasy/next";
export const middleware = withShipeasy(async (req) => {
// …your logic; return a NextResponse or nothing to continue
});next is a peer of this package only — it is not a peer of @shipeasy/sdk.
Express / Node
configure() kicks off a one-shot fetch, so the first new Client(user)
resolves against real rules with no extra wiring. For a long-running server that
should keep rules fresh, pass poll: true so the SDK runs the background refresh
for you — you never call an init method yourself.
import express from "express";
import { configure, Client } from "@shipeasy/sdk/server";
// Once, at boot — poll: true keeps rules fresh on a long-running server:
configure({
apiKey: process.env.SHIPEASY_SERVER_KEY!, // SERVER key
attributes: (u) => ({ user_id: u.id, plan: u.plan }),
poll: true,
});
const app = express();
app.get("/checkout", (req, res) => {
const flags = new Client(req.user); // construct once per request (cheap)
if (flags.getFlag("new_checkout")) return res.render("checkout-v2");
res.render("checkout");
});For non-React SSR (Express + a template engine) you can still emit the bootstrap
tags: se.getBootstrapTags() returns the same two tags as an HTML string, and
se.getDevtoolsTag() the devtools overlay tag. Both take their values from the
shipeasy() config, so neither needs an argument.
Pass cookies so logged-out bucketing stays stable
shipeasy() buckets anonymous visitors on the __se_anon_id cookie. Next.js
hands it over ambiently (next/headers); every other server has to pass it,
or each render mints a fresh id and re-buckets — a logged-out visitor can see a
different rollout or experiment variant on every page load:
app.get("/", async (req, res) => {
const se = await shipeasy({
serverKey: process.env.SHIPEASY_SERVER_KEY!,
cookies: req.headers.cookie, // ← the whole fix
});
res.send(render({ tags: se.getBootstrapTags() }));
});cookies takes whatever your framework already has:
| Shape | Where it comes from |
|---|---|
Cookie: header string | req.headers.cookie (Express/Nest/Fastify), c.req.header("cookie") (Hono), event.node.req.headers.cookie (Nitro) |
a WHATWG Request | Workers, Hono, Remix — also enables ?se_ks_* URL overrides with no middleware |
{ get(name) } accessor | Next's own cookies(), or any wrapper shaped like it |
It also unlocks the signed se_ov devtools override cookie on these servers.
Requests that resolve a real user_id are unaffected — an explicit identity
short-circuits anonymous bucketing entirely.
Cloudflare Workers
The server build runs on workerd. Configure once at module scope; bind per
fetch. Disable per-evaluation telemetry on hot paths — each beacon is an
outbound subrequest (cap 50 free / 1000 paid per invocation).
import { configure, Client } from "@shipeasy/sdk/server";
configure({
apiKey: globalThis.SHIPEASY_SERVER_KEY ?? "", // SERVER key (from env binding)
disableTelemetry: true, // hot path — skip per-eval beacons
});
export default {
async fetch(req: Request, env: Env): Promise<Response> {
const flags = new Client({ user_id: env.USER_ID }); // construct once per request
const v2 = flags.getFlag("new_checkout");
return new Response(v2 ? "v2" : "v1");
},
};Workers isolates are short-lived —
configure()'s one-shot fetch warms the blob; the CDN response is cached, so cold starts stay cheap. Usepoll: trueonly on long-lived Node servers, not per-request Worker isolates.
Browser / React ("use client")
When you ship the SDK to the browser without Next.js SSR (a plain SPA or a React app), configure once at app startup with the public client key:
"use client";
import { useEffect } from "react";
import { configure, Client } from "@shipeasy/sdk/client";
// Once, at app startup (e.g. root useEffect):
function bootstrap() {
configure({
clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY!, // public CLIENT key
attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan }),
});
}
// Per visitor:
const flags = new Client(currentUser); // construct once per visitor
await flags.ready(); // optional — await first /sdk/evaluate
flags.getFlag("new_checkout");The browser is single-user: new Client(user) runs the transform and
identify()s the result, merging browser context (locale, timezone,
path, referrer, screen_*, user_agent) and a persisted anonymous_id.
No-bundler script tag
For sites without a build step, drop in /sdk/boot.js — no npm install, no
configure() call. The edge evaluates your flags for the visitor and ships the
answers with the runtime, so one blocking request installs a fully-populated
window.shipeasy before first paint. Nothing to await, and no flash of default
values:
<script>
(function () {
var m = document.cookie.match(/(?:^|; )__se_anon_id=([^;]*)/),
a = m ? m[1] : crypto.randomUUID();
if (!m)
document.cookie =
"__se_anon_id=" + a + ";path=/;max-age=31536000;samesite=lax" +
(location.protocol === "https:" ? ";secure" : "");
document.write(
'<script src="https://cdn.shipeasy.ai/sdk/boot.js' +
'?p=<project-id>&k=sdk_client_...&a=' + encodeURIComponent(a) +
'"><\/script>'
);
})();
</script>
<script>
if (window.shipeasy.getFlag("new_checkout")) { /* … */ }
</script>The preamble mints the anonymous bucketing id first. It has to: the cookie is
first-party to your domain and never reaches our CDN, so boot.js can only
learn the id from the URL — and without a stable one, a visitor re-buckets on
every navigation. Add &u=<user_id> and &attrs=<json> for identity and
targeting.
window.shipeasy carries the same reads as the npm surface:
window.shipeasy.getFlag("new_checkout");
window.shipeasy.getConfig("checkout_copy");
window.shipeasy.getKillswitch("payments"); // or ("payments", "apple_pay")
window.shipeasy.universe("hero_cta").assign().get("primary_label", "Sign up");
window.shipeasy.identify({ user_id: "u-1", plan: "pro" });
window.shipeasy.track("checkout_started", { value: 49 });see() is the one thing the script tag does not carry — structured error
reporting needs the full client, so import { see } from "@shipeasy/sdk" on a
bundled page. Everything else, including the i18n loader and the devtools
overlay, works on a script-tag page with no second setup step.
React Native / Expo
The @shipeasy/sdk/client build is React Native safe. Metro resolves the
package's react-native condition to the same client build, and the SDK detects
the absence of a DOM at runtime, so configure() / new Client(user) /
getFlag / getConfig / universe().assign() / track / see all work over fetch
— no polyfills, no react-native-url-polyfill, no window/document shims.
import AsyncStorage from "@react-native-async-storage/async-storage";
import { configure, Client } from "@shipeasy/sdk/client";
// Once, at app startup:
configure({
clientKey: process.env.EXPO_PUBLIC_SHIPEASY_CLIENT_KEY!, // public CLIENT key
attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan }),
// Persist the anonymous id so bucketing is stable across app launches
// (there is no cookie / localStorage in React Native). Optional.
anonymousStore: {
get: (k) => AsyncStorage.getItem(k),
set: (k, v) => AsyncStorage.setItem(k, v),
remove: (k) => AsyncStorage.removeItem(k),
},
});
// Per user:
const flags = new Client(currentUser); // construct once per visitor
await flags.ready(); // optional — await first /sdk/evaluate
if (flags.getFlag("new_checkout")) { /* … */ }anonymousStore is the one piece worth wiring: its get/set/remove
(sync or async — the SDK awaits either) back the anonymous id with a real store,
so await flags.ready() resolves against a stable id that survives restarts. On
a fresh install the SDK mints an id and persists it; on later launches it adopts
the stored one. Omit it and the anon id simply regenerates per session — passing
a stable user_id via attributes also gives you durable bucketing.
What else differs from a browser (all graceful — the SDK degrades, never throws):
- No DOM lifecycle listeners. There is no
beforeunload/visibilitychangein React Native, so the event buffer flushes on its 5s timer and on explicittrack()— not on tab-hide. - Auto web-vitals and loader-driven i18n are skipped — they are DOM-only.
Flags, configs, experiments,
track(), andsee()error reporting are unaffected. The devtools overlay has a native counterpart — mount@shipeasy/react-native-devtools(see its page) instead of the browser bundle.
Where to go next
See Configuration for the full attributes, identity, and
SSR-bootstrap reference, and Testing for the network-free
configureForTesting() / configureForOffline() siblings.
Overview
@shipeasy/sdk is the TypeScript / JavaScript SDK for the Shipeasy hosted platform — feature gates, runtime configs, kill switches, A/B experiments, metrics,…
Configuration
Configure the SDK once at app boot, then evaluate per user with new Client(user). configure() builds the process-wide machinery (HTTP + blob cache + poll…