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…
/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)beforeconfigure(), 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:
| Option | Controls | Default |
|---|---|---|
isNetworkEnabled | Any 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 |
disableTelemetry | Just the per-evaluation usage telemetry beacon ("tracking"/outside logging). | telemetry ON in prod, OFF otherwise |
Production is inferred, in order:
SHIPEASY_ENV, thenNODE_ENV— a value ofproduction/prod⇒ production.- When neither is set (e.g. a Cloudflare Worker, or the browser, where there is
no native
NODE_ENV), the SDK's ownenvoption is used — and it defaults to"prod". So a real production deploy stays on by default; setenv: "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 method | Defaults 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)
| Variable | Side | Purpose |
|---|---|---|
SHIPEASY_SERVER_KEY | server | server key for configure({ apiKey }) / shipeasy({ serverKey }) |
NEXT_PUBLIC_SHIPEASY_CLIENT_KEY | browser | public client key for configure({ clientKey }) |