Shipeasy
SDKsReferenceTypeScript / JavaScript

Installation & configuration

One npm package, two entrypoints — @shipeasy/sdk/server (Node / Cloudflare Worker / Deno, server key) and @shipeasy/sdk/client (browser, public client key).…

Generated from the SDK's own /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 attributes transform, the identity default, and init-vs-one-shot live.

Install

npm install @shipeasy/sdk
# or
pnpm add @shipeasy/sdk
# or
yarn add @shipeasy/sdk

One 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:

PackagePeersFor
@shipeasy/nextnextedge middleware that mints the __se_anon_id bucketing cookie
@shipeasy/openfeature@openfeature/server-sdk / -webOpenFeature providers (/server, /web)
@shipeasy/react-native-devtoolsreact, 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 /client build.
  • 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 pass clientKey to 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).

OptionSideDefaultPurpose
apiKeyserverserver key (required on @shipeasy/sdk/server)
clientKeybrowserpublic client key (required on @shipeasy/sdk/client)
attributesbothidentity(yourUser) => ({ user_id, anonymous_id?, ...targeting }) — runs once per new Client(user). Omit ⇒ identity transform (you must pass the attribute bag verbatim).
pollserverfalsetrue ⇒ 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.
initservertrueOne-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.
baseUrlbothhttps://cdn.shipeasy.aioverride the CDN/edge base
envserverprodwhich published env to read (dev / staging / prod)
disableTelemetrybothfalseturn 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.
privateAttributesboth[]attribute names usable for targeting but stripped from every outbound track() payload (LD/Statsig privateAttributes).
stickyStoreservera 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" below

1. 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.

@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:

ShapeWhere it comes from
Cookie: header stringreq.headers.cookie (Express/Nest/Fastify), c.req.header("cookie") (Hono), event.node.req.headers.cookie (Nitro)
a WHATWG RequestWorkers, Hono, Remix — also enables ?se_ks_* URL overrides with no middleware
{ get(name) } accessorNext'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. Use poll: true only 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/visibilitychange in React Native, so the event buffer flushes on its 5s timer and on explicit track() — not on tab-hide.
  • Auto web-vitals and loader-driven i18n are skipped — they are DOM-only. Flags, configs, experiments, track(), and see() 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.

Was this page helpful?
Updated July 26, 2026

On this page