Shipeasy
SDKsReferenceTypeScript / JavaScript

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…

Generated from the SDK's own /docs/ — also served raw at https://shipeasy-ai.github.io/sdk-ts/pages/configuration.md.

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 lifecycle) and registers your attributes transform.

Server

import { configure, Client } from "@shipeasy/sdk/server";

configure({
  apiKey: process.env.SHIPEASY_SERVER_KEY!, // the SERVER key
  attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan, country: u.geo.country }),
});

// Per request:
const flags = new Client(req.user);
if (flags.getFlag("new_checkout")) { /* ... */ }

configure() kicks off a one-shot fetch so the first new Client(user).getFlag(...) resolves against real rules with no extra wiring. For a long-running server that should keep rules fresh, pass poll: true and the SDK runs the background refresh for you:

configure({ apiKey: process.env.SHIPEASY_SERVER_KEY!, poll: true });

Browser

import { configure, Client } from "@shipeasy/sdk/client";

configure({
  clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY!, // the CLIENT key
  attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan }),
});

const flags = new Client(currentUser);
await flags.ready(); // optional — await the first /sdk/evaluate round-trip
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.

Fail-safe reads & the logLevel option

Every runtime method the SDK exposes — getFlag, getFlagDetail, getConfig, universe(name).assign(), getKillswitch, track, and see() — is guaranteed to never throw into your code. If anything goes wrong internally (a bad decode callback, a malformed value, an unexpected state) the call fails silently: it returns the documented safe default (false / undefined / the not-enrolled result) and reports the swallowed error on console so you still find out. A feature-flag read can never take down a request.

logLevel controls how loud that reporting is:

configure({
  apiKey: process.env.SHIPEASY_SERVER_KEY!,
  logLevel: "warn", // default — "silent" | "error" | "warn" | "info" | "debug"
});

Ordering is silent < error < warn < info < debug; a level prints everything at or below it. The default "warn" prints error + warn and stays quiet otherwise. Pass "silent" to mute the SDK entirely. The same option exists on the browser configure({ clientKey, logLevel }) and on the SSR shipeasy({ serverKey, logLevel }) helper.

When one of these last-resort guards catches an internal SDK failure — a bug on our side, not yours — the SDK also reports it to Shipeasy's own project so we can find and fix SDK bugs across the apps that run it. This never touches your project or your Errors tab, carries no user/app data beyond the error itself, and is fire-and-forget (it can never slow down or break a read). It is on by default; opt out with disableInternalErrorReporting: true on any of the configure({ apiKey }), browser configure({ clientKey }), or shipeasy({ serverKey }) entry points.

Setup is deliberately still loud: constructing new Client(user) before configure(), or loading a bad offline snapshot, throws — those are boot-time misconfigurations you want to see immediately, not per-request runtime reads.

The attributes transform

attributes maps your user object into the Shipeasy attribute bag that every flag / experiment evaluation sees. It runs once per new Client(user).

type AttributesFn<U> = (user: U) => User; // User = { user_id?, anonymous_id?, ...targeting }

When you omit attributes, the transform is the identity function — the object you pass to new Client(...) is used verbatim, so it must already be the attribute bag ({ user_id, anonymous_id, ...targeting }).

Identity / bucketing unit

Bucketing hashes on user_id (falling back to anonymous_id). To bucket on a different attribute (e.g. company_id), the experiment carries a bucketBy — make sure your attributes transform surfaces that attribute. See Advanced.

Network & telemetry defaults (environment-derived)

The SDK is quiet by default outside production — an app that embeds it never phones home from a local dev machine or a CI run. Two switches control egress, and both default to ON in production and OFF everywhere else:

OptionControlsDefault
isNetworkEnabledAny outbound request — flag/experiment fetches, track(), exposure logging, see() reports, usage telemetry, internal error self-monitoring. When false the SDK is fully offline: reads return code defaults / overrides.true in prod, false otherwise
disableTelemetryJust the per-evaluation usage telemetry beacon ("tracking"/outside logging).telemetry ON in prod, OFF otherwise

Production is inferred, in order:

  1. SHIPEASY_ENV, then NODE_ENV — a value of production/prod ⇒ production.
  2. When neither is set (e.g. a Cloudflare Worker, or the browser, where there is no native NODE_ENV), the SDK's own env option is used — and it defaults to "prod". So a real production deploy stays on by default; set env: "dev" (or pass the switch explicitly) to keep a non-standard build quiet.

Pass either option explicitly to override the environment default:

// Force the SDK fully offline regardless of environment (no requests at all):
configure({ apiKey: process.env.SHIPEASY_SERVER_KEY!, isNetworkEnabled: false });

// Keep flag fetching but never emit usage telemetry:
configure({ apiKey: process.env.SHIPEASY_SERVER_KEY!, disableTelemetry: true });

Both options exist identically on the browser configure({ clientKey, … }) and the SSR shipeasy({ serverKey, … }) entry points. isNetworkEnabled: false is the production-safe equivalent of the test/offline modes below.

Test & offline configuration

For tests, swap configure() for configureForTesting() (no network, seed overrides) or configureForOffline() (evaluate real rules from a captured snapshot). Both replace the active configuration and are read through the same new Client(user) — see Testing.

SSR bootstrap (flags on first paint)

Server-render evaluated flags / configs / experiments so the browser SDK reads them synchronously on first paint — no flash, no extra round-trip. The shipeasy() server handle emits two declarative <script> tags. No SDK key is embedded in the bootstrap tag.

// app/layout.tsx — Next.js root layout (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 non-React SSR (Express, raw templates), se.getBootstrapTags() returns the same two tags as an HTML string. See i18n for the loader details.

Every emitted tag reads the config

clientKey, projectId, cdnBaseUrl and i18nDefaultProfile on shipeasy() are the defaults every tag carries, so the emit calls take no arguments. Pass an emit option only to override one tag:

Handle methodDefaults from shipeasy()
se.getBootstrapData() / se.getBootstrapTags()clientKey, i18nDefaultProfile, cdnBaseUrl (plus this request's anon id + identity)
se.getDevtoolsData() / se.getDevtoolsTag()projectId, clientKey, cdnBaseUrl

Devtools overlay tag

se.getDevtoolsTag() emits the hosted devtools overlay bundle (se-devtools.js) — nothing to install, no overlay code in your bundle. It reads the project id and public client key off the tag and opens with Shift+Alt+S or on any page loaded with ?se=1. It is deferred unless you pass { defer: false }: a developer tool never belongs on the critical rendering path.

const dev = se.getDevtoolsData();
<script src={dev.src} {...dev.attrs} />

Adding it unconditionally is fine: the overlay only opens for someone with a signed-in Shipeasy session, so on a page where nobody has authenticated it renders nothing and says nothing. Gating it on your own staff or environment check is optional — worth it only if you'd rather the bundle not load for end users at all:

{isStaff && <script src={dev.src} {...dev.attrs} />}

The standalone getDevtoolsTag(opts) / getDevtoolsData(opts) exports build the same tag without a request handle. See Browser devtools.

Server identity (no anon→identified flip)

By default the SSR bootstrap above evaluates anonymously — the root layout has no user, so the browser SDK seeds anonymous flags and then flips when its own identify() resolves. Register a server identity resolver and every server evaluation (nav reads and the bootstrap tag) runs against the identified user, so client and server agree and there is no flip. Identity comes from your own session — the authoritative, unspoofable source.

Register it once, outside your render path (Next instrumentation.ts, a server bootstrap module). The SDK calls it per request; read your session inside:

// instrumentation.ts — runs once at server startup; the resolver runs per request
import { setServerIdentity } from "@shipeasy/sdk/server";
import { auth } from "@/auth";

export async function register() {
  setServerIdentity(async () => {
    const session = await auth(); // reads this request's session cookie
    return session?.user?.email
      ? { user_id: session.user.email, email: session.user.email }
      : null; // null ⇒ anonymous request
  });
}

shipeasy() in your layout needs no change — it picks up the registered resolver automatically. Precedence, highest first: an explicit user passed to shipeasy({ user }) › the identify option passed to shipeasy({ identify }) (per-call) › the registered resolver › the signed __se_id cookie › __se_anon_id. A resolver that returns null or throws leaves the request anonymous — it never breaks the render. Flags are UX, never authorization: the client can only be more restrictive than the server, never grant itself a flag the server denied.

The identified user rides the bootstrap tag (data-user), so the browser SDK adopts it on first paint — no identify() call needed for the flags to be this user's from the start. If the browser also calls flags.identify(), it reconciles idempotently: a call matching the server's identity is a no-op (no extra /sdk/evaluate, no flip); a genuine change (new attributes or a different user_id) re-evaluates. data-user carries the user's traits (PII) — anonymous requests emit none.

Environment variables (convention)

VariableSidePurpose
SHIPEASY_SERVER_KEYserverserver key for configure({ apiKey }) / shipeasy({ serverKey })
NEXT_PUBLIC_SHIPEASY_CLIENT_KEYbrowserpublic client key for configure({ clientKey })
Was this page helpful?
Updated July 26, 2026

On this page