ShipEasy

SDKs

One package, two builds — server and browser. Plus framework adapters, native runtimes, and a DevTools overlay.

Production readyOn this page · 9 min readUpdated · May 3, 2026Works with · JS · TS · React · Vue · Svelte · Angular · RN · Ruby · Python · Go

The TypeScript SDK ships in @shipeasy/sdk with conditional exports for Node, ShipEasy, Bun, Deno, and the browser. Bundlers pick the right build automatically; you can also import the explicit subpath if you want to be unambiguous.

Other languages have first-party adapters or community ports — see Ruby, Python & Go below.

Server SDK

Use this on your backend (Node, Workers, Bun, Deno, Next.js Server Components, etc.). The server SDK polls flag and experiment blobs in the background and evaluates locally — there is no per-request network call from your code.

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

await shipeasy({
  apiKey: process.env.SHIPEASY_SERVER_KEY!,
  env: "prod", // "dev" | "staging" | "prod"
  baseUrl: "https://edge.shipeasy.dev", // optional override
});

shipeasy() is idempotent — call it from the framework entry point that runs once per cold start (Next.js root layout.tsx, an Express app initialiser, your Worker fetch handler).

Evaluating a gate

const enabled = flags.gate("new-checkout-flow", {
  user_id: "u_4f2a",
  plan: "pro",
  country: "US",
});

gate() is synchronous. The rule set lives in process memory. Pass any user attributes you want available to the targeting rules — see User attributes for the full guide.

Reading a dynamic config

const pricing = flags.config<{ base: number; currency: string }>("pricing");
const base = pricing?.base ?? 9.99;

Configs are typed via the optional generic. Always provide a fallback — your code should never crash because the SDK hasn't initialised yet.

Logging an event

flags.track("u_4f2a", "purchase", { value: 49.99, sku: "SHIRT-L-BLUE" });

track() is fire-and-forget. Events are batched and sent to /collect on the ShipEasy.

Resolving an experiment

import { experiments } from "@shipeasy/sdk/server";

const result = experiments.assign<{ color: "blue" | "green" }>("checkout-button-color", {
  user_id: "u_4f2a",
});

if (result.inExperiment) {
  // result.group       e.g. "control" | "variant_a"
  // result.params.color
}

Assignment is deterministic by user_id (or whatever you set as bucket_by). Calling assign() also queues an exposure event that the analysis pipeline reads from events store.

Translating a label

import { t } from "@shipeasy/sdk/server";

const greeting = t("home.hero.greeting", "Welcome back");

The t() function is part of the same SDK — no separate i18n init. Configuration happens once via the same shipeasy() call.

Browser SDK

Use this in the browser (Vite, Webpack, Next.js client, plain HTML). The browser SDK manages an anonymous_id cookie, fetches an evaluation bundle on init, and batches event uploads with navigator.sendBeacon on page hide.

import { shipeasy } from "@shipeasy/sdk/client";

shipeasy({
  apiKey: import.meta.env.VITE_SHIPEASY_CLIENT_KEY,
});

await shipeasy.identify({ user_id: "u_4f2a", plan: "pro" });

if (shipeasy.gate("new-checkout-flow")) {
  // …
}

shipeasy.track("checkout_viewed", { source: "nav" });

Server-side rendering bootstrap

For Next.js / Remix / SvelteKit, render the bundle into the page to avoid the client roundtrip:

// Server: build the bootstrap payload
import { buildClientBootstrap } from "@shipeasy/sdk/server";

const bootstrap = await buildClientBootstrap({ user: { user_id } });

// Client: hydrate with it
import { shipeasy } from "@shipeasy/sdk/client";

shipeasy({ apiKey });
shipeasy.initFromBootstrap(bootstrap);

This pattern lets you render the right variant on the server and keep the client in sync — no flicker. ShipEasy auto-handles the RSC async-context boundary so the bundle survives SSR streaming.

Framework usage

ShipEasy ships a single SDK — @shipeasy/sdk — that works from plain JavaScript. Every API (gate, config, experiment, t, track) is callable directly from React, Vue, Svelte, Angular, or vanilla JS without any framework-specific wrapper.

React
import { useEffect, useState } from "react";
import { shipeasy, gate, t } from "@shipeasy/sdk/client";

shipeasy({ apiKey: KEY, user: { user_id } });

function CheckoutButton() {
  const [enabled, setEnabled] = useState(() => gate("new-checkout-flow"));
  useEffect(() => shipeasy.subscribe(() => setEnabled(gate("new-checkout-flow"))), []);
  return <button>{t("checkout.cta", "Pay")}</button>;
}

The same pattern applies in Vue (watchEffect), Svelte ($:), or any framework — wrap shipeasy.subscribe in whatever reactivity primitive your framework provides.

A note on vanilla JS

Every ShipEasy SDK API works from plain JavaScript. You can drop the SDK into a plain <script> tag, an Astro island, an HTMX page, or a vanilla TS app, and every primitive — gates, configs, experiments, i18n — works without any framework.

React Native & mobile

Use the server build in React Native — the client build assumes a DOM:

import { shipeasy } from "@shipeasy/sdk/server";

Provide a stable anonymous_id — for example a UUID stored in AsyncStorage. The SDK has zero DOM dependencies in this build and works fine in the Hermes engine.

For native iOS and Android, use the JS SDK from the React Native bridge or call the same /sdk/* endpoints directly from Swift/Kotlin — the wire format is documented at Reference → SDK protocol.

Ruby

A Ruby gem is published as shipeasy-sdk on RubyGems:

require "shipeasy-sdk"

Shipeasy.configure do |c|
  c.api_key    = ENV.fetch("SHIPEASY_SERVER_KEY")
  c.public_key = ENV.fetch("SHIPEASY_CLIENT_KEY") # for the i18n view helpers
  c.profile    = "default"
end

if Shipeasy.flags.get_flag("new-checkout-flow", { user_id: "u_4f2a", plan: "pro" })
  # …
end

The gem mirrors the JS API surface. Polling, evaluation, exposure events, and i18n all behave the same.

Rails

In a Rails app, drop the Shipeasy.configure block above into config/initializers/shipeasy.rb and you're done — the gem auto-mounts i18n_head_tags, i18n_inline_data, i18n_script_tag, and i18n_t view helpers via a Railtie. In plain Ruby (Sinatra, Hanami, scripts) the Rails surface is skipped automatically.

Python & Go

In closed beta. Same wire format, same mental model. Reach out on the dashboard if you want early access.

Python (preview)
from shipeasy import shipeasy, flags

shipeasy(api_key=os.environ["SHIPEASY_SERVER_KEY"], env="prod")

if flags.gate("new-checkout-flow", user_id="u_4f2a", plan="pro"):
    ...
Go (preview)
client, _ := shipeasy.New(shipeasy.Config{
    APIKey: os.Getenv("SHIPEASY_SERVER_KEY"),
    Env:    "prod",
})

if client.Gate("new-checkout-flow", shipeasy.User{ID: "u_4f2a", Attrs: map[string]any{"plan": "pro"}}) {
    // ...
}

DevTools overlay

Add ?shipeasy=1 to any URL on a site running the browser SDK and a debugging overlay appears. You can inspect every gate, every config, every label, and override values locally — no rule changes, no redeploys, scoped to your browser only.

import { loadDevtools } from "@shipeasy/sdk/client";

if (process.env.NODE_ENV !== "production") loadDevtools();

Useful for QA, dogfooding new variants, walking a customer through what they should be seeing, and inspecting the runtime variables a label or config is using.

The overlay is local-only

Overrides are stored in localStorage. They affect only the browser tab where they're set; analytics and exposure events are tagged so you can filter them out of analysis.

Don't put a server key in client code

The two key kinds aren't interchangeable. Server keys can read full payloads and write events; they don't belong in a bundle. Put the client key in the browser and the server key in your server runtime — both come from Project → SDK keys.

NEXT

Drive everything from your terminal.

The CLI does anything the dashboard does — flags, experiments, keys, i18n, MCP install. Plus JSON output for piping.

Install the SDK
$npm install @shipeasy/sdk
Or start with the CLI
$npm install -g @shipeasy/cli
Was this page helpful?✎ Edit on GitHub

On this page