# Shipeasy documentation Generated from https://docs.shipeasy.ai — do not edit by hand. Regenerate with `pnpm gen:llms`. Every authored page plus the generated CLI and MCP reference, in the site's own navigation order. The per-operation API reference and the per-language SDK reference are indexed in https://docs.shipeasy.ai/llms.txt and served page by page. --- ## Shipeasy Source: https://docs.shipeasy.ai Feature flags & configs, metrics & alerts, and bug + error capture — one SDK, one API key, for everything you ship. Feature flags, typed runtime configs, kill switches, metrics & threshold alerts, and bug + error capture — one SDK, one API key. Drive it by hand, by CLI, or by an AI agent. ### Start here - **[Quickstart](https://docs.shipeasy.ai/get-started/quickstart)** — Install, wire one `configure()` call, ship a flag — from zero to live in five minutes. - **[How it works](https://docs.shipeasy.ai/get-started/overview)** — Fast local reads, explicit edge writes, sub-second propagation. The mental model behind every product. - **[Browse the SDKs](https://docs.shipeasy.ai/sdks)** — One package for server + browser, plus native ports for Go, Python, Ruby, Java, Kotlin, PHP and Swift. ### Explore by product - **[Flags & Configs](https://docs.shipeasy.ai/flags)** — Feature flags for ramps, typed configs for values you change without a deploy, and killswitches for break-glass. - **[Metrics & Alerts](https://docs.shipeasy.ai/metrics)** — Turn events into metrics with a small DSL, watch them move as you ramp, and raise threshold alerts that file their own tickets. - **[Bugs & Requests](https://docs.shipeasy.ai/feedback)** — User-reported bugs, feature requests, and handled errors — captured in-app and routed into your tooling. - **[SDKs](https://docs.shipeasy.ai/sdks)** — One package, server + browser builds, plus native ports for Go, Python, Ruby, Java, Kotlin, PHP and Swift. - **[Assistant](https://docs.shipeasy.ai/assistant)** — Ask questions about your project, and let the assistant draft flags, configs, and measurement plans as editable cards. - **[Get started](https://docs.shipeasy.ai/get-started/overview)** — Install the SDK, authenticate, learn the core concepts, and hand setup to a coding agent. ### Or start from a goal Most teams arrive with an outcome in mind. Each path is an ordered set of pages that takes you from install to shipped. **Ship a feature behind a flag** - [Add the SDK & CLI](https://docs.shipeasy.ai/get-started/install) - [Your first feature flag](https://docs.shipeasy.ai/flags/gates/quickstart) - [Rules & audiences](https://docs.shipeasy.ai/flags/gates/targeting) - [Gradual rollout](https://docs.shipeasy.ai/flags/gates/rollouts) **Know whether the ramp is safe** - [Define a metric](https://docs.shipeasy.ai/metrics/quickstart) - [The metric DSL](https://docs.shipeasy.ai/metrics/grammar) - [Threshold alerts](https://docs.shipeasy.ai/metrics/alerts) **Catch bugs & errors in production** - [Add the report button](https://docs.shipeasy.ai/feedback/getting-started) - [In-app devtools overlay](https://docs.shipeasy.ai/feedback/devtools) - [Handled errors with see()](https://docs.shipeasy.ai/feedback/error-reporting) - [Threshold alerts → tickets](https://docs.shipeasy.ai/metrics/alerts) **Let an AI agent run it all** - [Install the MCP server](https://docs.shipeasy.ai/get-started/mcp) - [Set up your agent](https://docs.shipeasy.ai/get-started/agents) - [Drive it from the CLI](https://docs.shipeasy.ai/get-started/cli) ### Not sure which primitive? Inside **Flags & Configs** there are three primitives. This picker maps "I want to do X" to the right one. Full breakdown → [decision guide](https://docs.shipeasy.ai/flags/decision). ### Why Shipeasy - **One SDK** for feature flags, configs, kill switches, metrics, and feedback. Server _and_ client. - **One CLI** (`shipeasy`) for everything you can do in the dashboard. - **One MCP server** so your AI agent can do the boring setup for you. - **Sub-millisecond evaluation**: flags resolve in your code without a network round-trip. - **Export your data**: your events and metric series are yours, not locked into our dashboard. --- ## How it works Source: https://docs.shipeasy.ai/get-started/overview A tour of the moving pieces — two runtimes, one shared database, config blobs at the edge, and an SDK that never blocks your request. Shipeasy splits the world into two runtimes — an admin app and an edge worker — sharing one database and a handful of config blobs. Reading a flag or a config is a local memory lookup. Changing one purges a single CDN URL and propagates worldwide in under a second. Shipeasy is built around one rule: **the read path is fast and the write path is explicit.** Reading a flag or a config should never block your request. Changing one should be visible globally within a second of the dashboard click. That single rule decides almost every architectural choice — no per-request fetches, no TTL-based invalidation, no streaming sockets. Just config blobs at the edge, polled in the background, and purged on change. ### The two-runtime split [#two-runtimes] - **[Admin app — writes](#admin-app)** — Dashboard, REST API, Server Actions, sessions, and the CLI's programmatic surface. This is where humans (and the CLI) make changes. - **[Edge worker — reads](#edge-worker)** — Serves /sdk/flags, /sdk/experiments, /sdk/labels, ingests events at /collect, and runs the cron analysis pipeline. - **[Shared state — one source of truth](#shared-state)** — The database is the row store. KV is the read cache. The events store is append-only telemetry. All three are scoped per project. - **[Analysis — Cron + Queues](#analysis)** — A scheduled trigger enqueues one job per project. The consumer runs the t-test and writes results back to the database. #### Admin app [#admin-app] The admin app is a Next.js app that owns: - The dashboard UI for feature flags, configs, kill switches, metrics and alerts. - Server Actions and Route Handlers — the same endpoints the CLI calls. - Stateless JWT sessions (short expiry, no session table). - The KV rebuild + CDN purge pipeline. Whenever you change a flag, the admin app rebuilds the affected blob and purges the URL. Writes never go straight to KV from the dashboard. They go to the database first (the row of truth), then a rebuild helper reassembles the blob and writes it back, then an explicit purge invalidates the CDN. #### Edge worker [#edge-worker] The edge worker is a separate, read-mostly, stateless service. Two endpoint groups matter: - **`/sdk/*`** — what your SDK polls. Returns the config blob unchanged. Cached at the edge with a long TTL; the admin's purge step is what makes the cache eventually-consistent. - **`/collect`** — fire-and-forget event intake. Returns `202` immediately. Your code path doesn't wait on it. The same worker also handles the CLI device-auth flow and runs the cron + queue analysis pipeline. #### Shared state [#shared-state] - `Database (SQLite)` (row store) — Source of truth — feature flags, configs, kill switches, metrics, keys, exposures, daily results. Project-scoped on every query. - `KV` (read cache) — Two blobs per project: `:flags` (feature flags, configs and kill switches) and `:experiments`, the longer-lived half of the rule set. - `Events store` (event store) — Append-only telemetry. Written from the worker only, never from the admin app. - `Queues` (async pipeline) — The cron enqueues one message per project. The consumer runs analysis and writes results back to the database. ### The lifecycle of a write When you flip a flag, six things happen — all within about a second. The write path fans out from the admin app; the read path and event ingestion live entirely on the edge worker; analysis closes the loop back to the database overnight. **You change something in the dashboard or CLI** A flag flipped, a rollout bumped, a config published. The CLI hits the same Server Action the dashboard does — there is no second API. **The admin app writes the row to the database** The database is the source of truth. Every other surface (config blob, daily analysis row, dashboard table) is derived from a row. **The config blob is rebuilt** The rebuild helper assembles the full project blob — every feature flag, every config, every targeting rule — into a single JSON payload. Rebuild is cheap because the project is small; we don't do partial updates. **The blob is written to KV** KV propagation is sub-second between edge locations. The blob is small (a few KB for most projects, low MB for large ones). **The CDN URL is purged** Reads are cached at the edge with **infinite TTL**. The purge invalidates the single URL that points at the project's blob — every other project's cache stays warm. **Your SDK polls and picks up the change** Server SDKs poll on a plan-driven interval. Browser SDKs poll on the same cadence in the background, and re-evaluate on `identify(...)`. Total time-to-visible from the dashboard click is < poll interval + ~100ms of CDN propagation. > **Why infinite TTL and not, say, 30 seconds?** TTL-based invalidation makes the worst-case latency equal to the TTL. Explicit purge makes the worst-case latency equal to the CDN propagation time, which is sub-second worldwide. The only downside — coordinating the purge — is a problem the admin app already solves on every write. ### The lifecycle of a read The other direction is much shorter — and on purpose. **Your code asks the SDK** `new Client(user).getFlag("new-checkout")`. Synchronous. No Promise. **The SDK evaluates locally** The full rule set for every feature flag in your project lives in process memory. Targeting rules and rollout buckets are evaluated against the user object you passed in. Bucketing is deterministic — same user, same answer, every time. **A background poll keeps the bundle fresh** A worker thread (Node) or `setInterval` (browser) re-fetches the config blob on the plan-driven cadence. If the body is unchanged the SDK does nothing; if it changed, the in-memory rule set swaps atomically. **Exposure events are batched** An evaluation queues a small exposure event recording what the user was shown. Events are flushed to /collect in batches — `sendBeacon` on page hide in the browser, periodic flush + on-process-exit on the server. There is no per-evaluation network call, no rate limit on `getFlag()`, and no async surface to wrap. The cost of an evaluation is approximately the cost of a hash plus a few comparisons. ### Two SDK builds, one package `@shipeasy/sdk` ships **server** and **browser** builds in the same npm package, picked by your bundler via conditional exports (`node` → `dist/server`, `browser` → `dist/client`). Both share the same evaluation core, but differ in their environment assumptions: - `Server build` (@shipeasy/sdk/server) — For Node, Workers, Bun, Deno, RSC. Polls in the background. Bind a `Client` to the user, then read with no per-call user argument. - `Client build` (@shipeasy/sdk/client) — For browsers. Manages an `anonymous_id` cookie. Identifies once, reuses the user. Ships a devtools overlay. For the native server SDKs (Go, Python, Ruby, Java, Kotlin, PHP, Swift) the evaluation model is identical — same blob, same deterministic bucketing. See the [SDKs](https://docs.shipeasy.ai/sdks) reference. ### Plan-driven knobs A handful of behaviours are plan-derived rather than per-project. The big one is the **SDK poll interval** — the worst-case lag between an edit going live at the edge and a given SDK instance noticing it. Evaluation itself is local and instant on every plan; only the refresh cadence moves. You never configure it. The worker advertises the current value in the `X-Poll-Interval` response header and the SDK re-paces itself on the next poll, so a plan change propagates through the next KV rebuild with no redeploy and no per-project migration. Your plan's interval is on the [pricing page](https://shipeasy.ai/pricing) and on **Settings → Billing**. ### Identity model A two-tier identity is enough for almost every use case: ```ts import { configure, Client } from "@shipeasy/sdk/client"; configure({ clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY ?? "", attributes: (u) => ({ user_id: u.id, // your stable user ID, set after login plan: u.plan, country: u.country, beta_tester: u.betaTester, }), }); // Bind a client to the current user; getters take no user argument. const flags = new Client(currentUser); ``` Shipeasy buckets by `user_id ?? anonymous_id`, so a user gets a stable assignment before _and_ after login. The `anonymous_id` is auto-managed by the browser SDK (first-party cookie). When `identify(...)` runs after anonymous activity, the SDK emits an internal alias record so the daily analysis stitches the pre-login exposures to the post-login `user_id` — no explicit alias call required. For B2B, you can bucket by `company_id` instead, so all teammates see the same variant. See [Identity & bucketing](https://docs.shipeasy.ai/get-started/identity-and-bucketing) for the full guide. ### What we deliberately don't do > **Trade-offs in plain English** - **No per-request fetch from the SDK.** The bundle is in process memory. - **No TTL-based KV invalidation.** Writes purge the affected URL explicitly. - **No streaming sockets.** Polling at plan interval is good enough for flag changes and removes a class of operational headaches. - **No vendor lock-in for your data.** Events and metric series are plain rows in your database — exportable, queryable, yours. ### Where to next - **[Install](https://docs.shipeasy.ai/get-started/install)** — Add the packages to your project and your machine. - **[Quickstart](https://docs.shipeasy.ai/get-started/quickstart)** — The shortest path from install to a flag in production. - **[Keys & environments](https://docs.shipeasy.ai/get-started/keys-and-environments)** — Which key goes where, and how environments are scoped. **Related** - [Evaluation & caching](https://docs.shipeasy.ai/get-started/evaluation-and-caching) — the read path in depth - [Identity & bucketing](https://docs.shipeasy.ai/get-started/identity-and-bucketing) — who gets what - [SDKs](https://docs.shipeasy.ai/sdks) — every language --- ## Quickstart (get-started) Source: https://docs.shipeasy.ai/get-started/quickstart Install the SDK and CLI, bind a project, wire one init call, ship a flag at 0%, and ramp — in about five minutes. One SDK, one CLI, one configure call. Create a flag at 0%, wrap your code, then ramp it from your terminal. No card required. This is the universal path. Every product — gates, configs, kill switches, metrics — starts here, then branches. If you only read one page, read this one. **Add the SDK and CLI** ```bash npm install @shipeasy/sdk && npm install -g @shipeasy/cli ``` One package, server **and** browser. The CLI is the `shipeasy` binary — it logs in through your browser, so there are no env tokens to copy. **Authenticate + bind a project** ```bash shipeasy login ``` Opens your browser, confirms, and writes a credential file to `~/.shipeasy/credentials` (mode `0600`). Then bind the working directory to a project so every command knows where it points: ```bash shipeasy bind my-project # or run inside a repo that already has a binding ``` Full flow, including CI tokens, lives in [Authenticate](https://docs.shipeasy.ai/get-started/authenticate). **Configure once, use everywhere** ```bash // app/layout.tsx — runs once per cold start\nimport { configure } from "@shipeasy/sdk/server";\nconfigure({ apiKey: process.env.SHIPEASY_SERVER_KEY ?? "", attributes: (u) => ({ user_id: u.id, plan: u.plan }) }); ``` The single `configure()` call boots flags, configs **and** kill switches. The server SDK polls the rule set in the background and evaluates **locally** — there is no per-request network hop. Env (dev / staging / prod) is derived from the key, not from a query param. See [Keys & environments](https://docs.shipeasy.ai/get-started/keys-and-environments). **Create a flag at 0%** ```bash shipeasy release flags create checkout-v2 --rollout-percent 0 ``` A flag at 0% is **off for everyone** but live in your rule set worldwide. You ship the code dark, then ramp when you're ready. Changes propagate in under a second (see [Evaluation & caching](https://docs.shipeasy.ai/get-started/evaluation-and-caching)). **Wrap code with getFlag** ```bash import { Client } from "@shipeasy/sdk/server";\n\nconst flags = new Client(currentUser);\nif (flags.getFlag("checkout-v2")) {\n return renderCheckoutV2();\n}\nreturn renderCheckoutV1(); ``` Bind a `Client` to the current user once; the getters take no user argument and bucket against the attributes your `configure()` transform resolved. The browser flow is identical — `new Client(user)`, then `flags.getFlag("checkout-v2")`. **Ramp it up** ```bash shipeasy release flags update checkout-v2 --rollout-percent 25 ``` Bump the rollout from your terminal (or the dashboard). The same deterministic bucketing means anyone in the first 25% stays in as you climb — nobody flickers out. Take it to `--rollout-percent 100` when you're confident. ### Configure and read a flag, in your language The runway above is TypeScript. The same two moves — configure once with the **server** key, then read a flag locally — exist in every server SDK. Pick yours: **TypeScript** ```ts import { configure, Client } from "@shipeasy/sdk/server"; configure({ apiKey: process.env.SHIPEASY_SERVER_KEY ?? "" }); const flags = new Client(currentUser); if (flags.getFlag("checkout-v2")) { // ship it } ``` **Python** ```python import shipeasy shipeasy.configure(api_key=os.environ["SHIPEASY_SERVER_KEY"]) flags = shipeasy.Client(current_user) if flags.get_flag("checkout-v2"): ... ``` **Go** ```go import ( "os" shipeasy "github.com/shipeasy-ai/sdk-go" ) shipeasy.Configure(shipeasy.Options{APIKey: os.Getenv("SHIPEASY_SERVER_KEY")}) flags := shipeasy.NewClient(currentUser) if flags.GetFlag("checkout-v2") { // ship it } ``` **Ruby** ```ruby Shipeasy.configure do |c| c.api_key = ENV.fetch("SHIPEASY_SERVER_KEY") end flags = Shipeasy::Client.new(current_user) if flags.get_flag("checkout-v2") # ship it end ``` **Java** ```java import ai.shipeasy.Shipeasy; import ai.shipeasy.Client; Shipeasy.configure(System.getenv("SHIPEASY_SERVER_KEY")); Client flags = new Client(currentUser); boolean enabled = flags.getFlag("checkout-v2"); ``` **Kotlin** ```kotlin import ai.shipeasy.configure import ai.shipeasy.Client configure(System.getenv("SHIPEASY_SERVER_KEY")) val flags = Client(currentUser) flags.getFlag("checkout-v2") ``` **PHP** ```php use function Shipeasy\configure; use Shipeasy\Client; configure(getenv('SHIPEASY_SERVER_KEY')); $flags = new Client($currentUser); $enabled = $flags->getFlag('checkout-v2'); ``` **Swift** ```swift import Shipeasy configure(apiKey: ProcessInfo.processInfo.environment["SHIPEASY_SERVER_KEY"]!) let flags = try Client(currentUser) let enabled = await flags.getFlag("checkout-v2") ``` The browser build is separate — see the client init below. For the full per-language API, each SDK has its own page under [SDKs](https://docs.shipeasy.ai/sdks). ### What just happened **One key per side** The server passes its key as `apiKey`; the browser passes the public key as `clientKey`. They are never interchanged or passed together. The browser key is public and ships in your bundle; the server key is a secret. **Local, deterministic evaluation** Your SDK holds the rule set in memory and buckets each unit with a cross-language `murmur3` hash. The same user always lands in the same bucket, on every surface and in every language. **Sub-second propagation** Edits rebuild a KV blob and explicitly purge the CDN. Your SDK picks the change up on its next background poll — under a second to visible. > **Browser init looks the same** ```ts import { configure, Client } from "@shipeasy/sdk/client"; configure({ clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY ?? "", attributes: (u) => ({ user_id: u.id, plan: u.plan }) }); const flags = new Client(currentUser); await flags.ready(); if (flags.getFlag("checkout-v2")) {/* ship it */} ``` Same single configure call, public client key, then a user-bound `Client` whose getters take no user argument. ### Branch to your product You have a flag ramping. Pick where to go deeper. - **[Gates](https://docs.shipeasy.ai/flags/gates/quickstart)** — Targeting rules, per-condition rollouts, gradual ramps, and kill switches. - **[Configs](https://docs.shipeasy.ai/flags/configs/quickstart)** — Typed JSON values you change at runtime — limits, copy, thresholds — without a deploy. - **[Metrics & alerts](https://docs.shipeasy.ai/metrics/quickstart)** — Turn the events you already log into a number, then have a threshold rule watch it for you. - **[Kill switches](https://docs.shipeasy.ai/flags/killswitches/quickstart)** — One switch that turns a subsystem off everywhere, without a deploy or a rollout ramp. **Related** - [Install](https://docs.shipeasy.ai/get-started/install) — every package, every runtime - [Keys & environments](https://docs.shipeasy.ai/get-started/keys-and-environments) — server vs client key, env scoping - [Evaluation & caching](https://docs.shipeasy.ai/get-started/evaluation-and-caching) — the read path in depth - [MCP server](https://docs.shipeasy.ai/get-started/mcp) — let your AI assistant do the setup ``` --- ## Install Source: https://docs.shipeasy.ai/get-started/install Add the Shipeasy SDK to your application, the CLI to your machine, and (optionally) the MCP server to your AI assistant. Shipeasy ships as a small set of npm packages — install only the ones you need. There is one SDK, one CLI binary, and one MCP server. They all share the same login. ### Pick what you need - **[@shipeasy/sdk](#sdk)** — The core SDK. Conditional exports pick the right build for your runtime — Node, Workers, Bun, Deno, or browser. - **[@shipeasy/cli](#cli)** — The shipeasy command. Login, manage flags, configs and kill switches, work the ops queue, install the MCP server. - **[@shipeasy/mcp](#mcp)** — MCP server for AI assistants. Installed via the CLI — your agent gets a typed toolkit. - **[Framework adapters](#frameworks)** — Idiomatic wrappers around the browser SDK. Hooks, composables, stores, directives. ### Quick install ```bash npm install @shipeasy/sdk ``` ```bash npm install -g @shipeasy/cli shipeasy login ``` That's the whole runway from zero to working. Everything below is detail. ### SDK [#sdk] **Install the package** Pick your language — every tab pulls the install/registry line straight from that SDK's [reference page](https://docs.shipeasy.ai/sdks). **TypeScript** ```bash npm install @shipeasy/sdk ``` The package ships **both** server (Node, Workers, Bun, Deno) and browser builds via conditional exports. Your bundler picks the right one automatically; you can also import the explicit subpath (`/server`, `/client`) when you want to be unambiguous (e.g. inside a monorepo with a shared util used from both). **Python** ```bash pip install shipeasy ``` **Go** ```bash go get github.com/shipeasy-ai/sdk-go ``` **Ruby** ```ruby ## Gemfile gem "shipeasy" ``` **Java** ```xml ai.shipeasy shipeasy 0.1.0 ``` **Kotlin** ```kotlin implementation("ai.shipeasy:shipeasy-kotlin:0.3.0") ``` **PHP** ```bash composer require shipeasy/sdk ``` **Swift** ```swift dependencies: [ .package(url: "https://github.com/shipeasy-ai/sdk-swift.git", from: "0.1.0"), ] ``` **Server initialisation** For Next.js, put this in your root `layout.tsx` so it runs once per cold start. For an Express or app, call it during startup. For RSC, the SDK persists state across the async-context boundary so you don't need a Provider on the server side. ```ts import { configure } from "@shipeasy/sdk/server"; configure({ apiKey: process.env.SHIPEASY_SERVER_KEY ?? "", attributes: (u) => ({ user_id: u.id, plan: u.plan }), }); ``` The single `configure()` call boots flags, configs **and** kill switches. The optional `attributes` transform maps your user object onto the Shipeasy attribute map, so every bound `Client` you construct evaluates against the right context. The server SDK polls `/sdk/flags` and `/sdk/experiments` in the background. Evaluation happens **locally** — there is no per-request network call from your code. Env (dev / staging / prod) is **derived from the key**: each key is bound to exactly one environment when you mint it, so you deploy the prod key to prod and the staging key to staging — there is nothing to set in code. (A server key may still override per request with `?env=` for local debugging; client keys cannot — see below.) **Browser initialisation** ```ts import { configure, Client } from "@shipeasy/sdk/client"; configure({ clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY ?? "", attributes: (u) => ({ user_id: u.id, plan: u.plan }), }); // Once you know who the user is, bind a client and await freshness: const flags = new Client(currentUser); await flags.ready(); ``` The client SDK auto-manages an `anonymous_id` cookie, batches event uploads with `navigator.sendBeacon` on page hide, and exposes a [devtools overlay](https://docs.shipeasy.ai/get-started/sdks#devtools) at `?shipeasy=1`. The client key is public — it ships in your browser bundle. Its environment is **locked to the key** and cannot be changed at runtime, so a client key minted for `staging` can only ever read `staging` flags and configs. Use a **separate client key per environment**; never share one across environments expecting isolation. **One key, one configure call** Flags, configs, and kill switches share a single key per side (`apiKey` on the server, `clientKey` in the browser). There is no second configure step — the single `configure()` call boots all of them. Don't wrap this in a custom helper file. The SDK owns its own initialisation. > **Two kinds of keys — server vs client** **Server keys** can read full payloads and write events. **Client keys** are scoped: they expose only the feature flags and configs you mark _client-readable_, and they rate-limit by domain. Never put a server key in browser code. Manage both in **Project → SDK keys**, or with `shipeasy keys`. ### CLI [#cli] **Install globally** ```bash npm install -g @shipeasy/cli ``` Or skip the install and use it ad-hoc: ```bash npx -y @shipeasy/cli@latest --help ``` **Verify** ```bash shipeasy --version shipeasy --help ``` **Log in** ```bash shipeasy login ``` Opens a browser to confirm. After confirmation, credentials are written to `~/.shipeasy/credentials` (mode `0600`). See [Authenticate](https://docs.shipeasy.ai/get-started/authenticate) for the full flow including CI tokens. The CLI is a thin wrapper over the same Server Actions the dashboard uses. Anything you can do in the UI, you can do from a terminal or a CI job. Full reference at [CLI](https://docs.shipeasy.ai/get-started/cli). ### MCP server [#mcp] If you use Claude Code, Cursor, Windsurf, or any other MCP-compatible AI assistant, install the Shipeasy MCP server so your agent can do setup work for you: shipeasy mcp install \n? Which assistants? › Claude Code, Cursor\n✔ Wrote ~/.claude/settings.json\n✔ Wrote .cursor/mcp.json\nMCP server registered. Restart your AI assistant to pick it up. The MCP server uses your CLI credentials — no extra env vars, no separate token. See [MCP server](https://docs.shipeasy.ai/get-started/mcp) for the tool inventory and manual config. ### Environment variables The SDK reads the following from `process.env` (and `import.meta.env` in Vite). Anything passed explicitly to `configure({ ... })` wins. - `SHIPEASY_SERVER_KEY` (string) — Server-side SDK key. Used by the server build. Treat as a secret. - `NEXT_PUBLIC_SHIPEASY_CLIENT_KEY` (string) — Client-side SDK key. Safe to expose. Vite users: `VITE_SHIPEASY_CLIENT_KEY`. - `SHIPEASY_API_BASE_URL` (string) — Override the admin API base URL (CLI default `https://shipeasy.ai`). - `SHIPEASY_APP_BASE_URL` (string) — Override the dashboard URL the CLI links to (default `https://shipeasy.ai`). ### Frameworks [#frameworks] - **[Node · Workers · Bun · Deno](https://docs.shipeasy.ai/get-started/sdks#server)** — `@shipeasy/sdk/server` works in any V8/Node-compatible runtime out of the box. - **[React, Vue, Svelte, Angular](https://docs.shipeasy.ai/get-started/sdks#frameworks)** — Per-framework adapters that wrap the browser SDK with idiomatic primitives. - **[React Native, iOS, Android](https://docs.shipeasy.ai/get-started/sdks#mobile)** — Use the server build — it has zero DOM dependencies. - **[Ruby, Python, Go](https://docs.shipeasy.ai/get-started/sdks#ruby)** — The Ruby gem ships today. Python and Go are in beta — ping us for access. ### Edge runtimes & ESM The SDK is shipped as ESM-first with a CJS fallback for older Node. There are no Node built-ins on the hot path, so it runs unchanged on Shipeasy, Vercel Edge, Deno Deploy, and Bun. > **Conditional exports cheat sheet** - `import { configure, Client } from "@shipeasy/sdk/server"` — server build, picked automatically when bundling for Node/Workers. - `import { configure, Client } from "@shipeasy/sdk/client"` — browser build, picked when bundling for the browser. - `import "@shipeasy/sdk"` — re-exports both via conditional resolution. Use this only if your bundler honours `exports`. ### Monorepo notes In a pnpm/yarn workspace, install `@shipeasy/sdk` in each app that uses it (don't hoist it into the root unless your tooling resolves hoisted deps). For shared internal libraries that import from the SDK, depend on it as a `peerDependency` so consumers control the version. If you depend on the SDK from a Cloudflare Worker built with `CLI`, no special config is needed — `CLI` honours `exports` and picks the right build. ### Troubleshooting > **Node 18 or older** Shipeasy requires Node 20+. Older Node versions lack stable `fetch` and `AbortSignal.timeout`. Upgrade Node, or polyfill `fetch` and pass it explicitly via `configure({ fetch: customFetch })`. > **Edge runtime build picks the wrong file** Some bundlers misclassify edge targets as Node. Force the right build with the explicit subpath: `import { configure, Client } from "@shipeasy/sdk/server"` — this works in every edge runtime we've tested. > **Vite says "process is not defined"** Use `import.meta.env.VITE_SHIPEASY_CLIENT_KEY` rather than `process.env.*` in Vite, and let Vite inline it at build time. Don't pass `process.env.SHIPEASY_SERVER_KEY` to the client build — that key belongs only on the server. **Related** - [Keys & environments](https://docs.shipeasy.ai/get-started/keys-and-environments) — Which key goes where - [SDKs](https://docs.shipeasy.ai/get-started/sdks) — Server build, browser build, native ports - [Authenticate](https://docs.shipeasy.ai/get-started/authenticate) — Sign in the CLI and your agent - [Troubleshooting](https://docs.shipeasy.ai/get-started/troubleshooting) — When the install does not take --- ## Authenticate Source: https://docs.shipeasy.ai/get-started/authenticate One `shipeasy login` opens your browser, signs in the CLI and every MCP tool on your machine, and quietly refreshes itself for 30 days. Shipeasy uses an OAuth-style PKCE device-auth flow. Run one command, click one link in your browser, and the CLI plus every MCP tool on your machine is signed in. There is no API key to copy, paste, or rotate — for interactive use. For CI, you swap the device-auth flow for a long-lived `SHIPEASY_CLI_TOKEN` env var. Same code paths underneath; different credential source. ### Log in shipeasy login \n→ Opening https://shipeasy.ai/auth/cli/abc123 in your browser...\n→ Waiting for the browser flow to complete...\n✔ Authenticated as you@example.com (project: acme)\n✔ Credentials saved to ~/.shipeasy/credentials Your browser opens to a confirmation page. Sign in with GitHub, Google, or a magic link. When you confirm, the CLI command exits with success and the credentials are saved to `~/.shipeasy/credentials` (mode `0600`). > **No environment variables required** Once you've logged in, every CLI command and every MCP tool call picks up your credentials automatically. You can still set `SHIPEASY_CLI_TOKEN` if you prefer — it always wins over the file — but interactive use never needs it. ### Under the hood — device-auth flow **CLI requests a device code** The CLI calls `POST /auth/device/code` on the Shipeasy, which returns a short `device_code`, a longer `user_code`, and a `verification_uri`. **CLI opens the browser** The CLI prints the URL and tries to open it (`open`/`xdg-open`/`start`). If your terminal is headless, the URL is shown for you to paste. **You confirm in the browser** You sign in (or you're already signed in to the dashboard) and click **Confirm**. The browser hits `POST /auth/device/confirm` with the `device_code` and your session. **CLI polls for completion** The CLI polls `POST /auth/device/token` every couple of seconds. Once the browser confirms, the response includes an `access_token` (1h) and a `refresh_token` (long-lived, rotates on use). **Credentials are written to disk** The token pair lands in `~/.shipeasy/credentials`. Future CLI calls use the access token; if it's expired, the CLI refreshes transparently before the call. There is no plaintext password anywhere on disk. PKCE means an attacker who steals the URL still can't complete the flow without your verifier. ### Pick a project If your account has access to more than one project, the login flow lets you choose one. To switch later: ```bash shipeasy whoami # show the active project + accessible projects shipeasy bind # bind the cwd to a project (writes .shipeasy) shipeasy login --project # re-auth scoped to a different project ``` For one-off overrides on a single command, every CLI subcommand accepts a `--project ` flag: ```bash shipeasy release flags list ``` The order of precedence is: `--project` flag > `.shipeasy` file in the cwd > the project bound during `shipeasy login`. ### Multiple orgs Each Shipeasy project belongs to one org. If you're in several orgs, projects from all of them show up in `shipeasy whoami` output. There is no separate `org switch` — the project is the authoritative scope and the org is implied by the project ID. ### Log out ```bash shipeasy logout ``` This wipes `~/.shipeasy/credentials`. You'll need to `login` again before the next CLI or MCP call. To revoke the **server-side** session (e.g. you suspect the credential file leaked), use `shipeasy logout`. This calls the admin API to invalidate the refresh token before deleting the local file. After this, even an attacker with the credential file can't obtain new access tokens. ### Token rotation Refresh tokens rotate on every use. The pattern is: 1. CLI sees the access token is expired (or about to be). 2. CLI calls `POST /auth/device/refresh` with the current refresh token. 3. The server returns a **new** access token and a **new** refresh token, and invalidates the old refresh token. 4. The CLI writes the new pair to disk before doing anything else. This means a stolen refresh token is good for at most one refresh — the moment you next use the CLI on your laptop, the attacker's copy stops working. ### Using Shipeasy in CI For GitHub Actions, GitLab CI, and any non-interactive environment, generate a long-lived **API token** in **Project → Tokens → Create** and pass it as `SHIPEASY_CLI_TOKEN`: ```yaml title=".github/workflows/release.yml" - name: Check the flags this release depends on env: SHIPEASY_CLI_TOKEN: ${{ secrets.SHIPEASY_CLI_TOKEN }} run: shipeasy release flags list --json ``` API tokens skip the device-auth flow entirely. They're scoped to one project, can be marked **read-only** or **read-write**, and can be revoked with one click. > **Scope and rotate your CI tokens** Read-write API tokens can change anything in the project. Treat them like any other production secret: store in your CI vault, rotate on a schedule, scope to the smallest project that works, and never commit them. `shipeasy sdk keys list` and `shipeasy sdk keys revoke` let you audit and revoke at any time. ### SDK keys vs API tokens — pick the right one - `Server SDK key` (server runtime) — Read-only. Lets the server SDK fetch the rule blobs and ship exposure events. Cannot mutate. - `Client SDK key` (browser runtime) — Read-only and scoped — exposes only flags/configs marked client-readable. Domain-rate-limited. - `API token` (CLI / CI / programmatic) — Acts as a user. Read-only or read-write. Use these for CI, scripts, and human terminals (via `shipeasy login`). A common mistake is to use an API token in the SDK at runtime. Don't — they're much more powerful than they need to be, and they don't enforce per-domain rate limits. Use SDK keys for the SDK, API tokens for everything else. ### What gets stored on your machine ``` ~/.shipeasy/credentials # mode 0600, JSON ├─ access_token # short-lived (1h), refreshed automatically ├─ refresh_token # long-lived, rotates on every use ├─ project_id # active project ├─ user_email # for `whoami` └─ created_at # when this credential set was issued ``` The credentials file is the only state the CLI writes. Project/env preferences live alongside in `~/.shipeasy/config.json`. Neither file should ever be committed. ### Troubleshooting > **`shipeasy login` opens the wrong browser** Set `BROWSER=firefox` (or your browser of choice). The CLI honours the standard `BROWSER` env var. > **`shipeasy login` hangs in a remote SSH session** Pass `--no-browser` to print the URL instead of opening it. Open the URL on your local machine, confirm, and the CLI in the SSH session completes when the poll succeeds. > **MCP tools say `not authenticated`** Run `shipeasy login` in a real terminal first. The MCP server reads the same `~/.shipeasy/credentials` file the CLI writes. Some agents launch with a stripped env — set `HOME` explicitly in your MCP config if the tool can't find the file. **Related** - [Keys & environments](https://docs.shipeasy.ai/get-started/keys-and-environments) — The keys your app uses, not the CLI - [CLI](https://docs.shipeasy.ai/get-started/cli) — What the session unlocks - [MCP server](https://docs.shipeasy.ai/get-started/mcp) — The same login, for your agent - [Team & permissions](https://docs.shipeasy.ai/get-started/team) — Who may publish to production --- ## SDKs (get-started) Source: https://docs.shipeasy.ai/get-started/sdks One package, two builds — server and browser. Plus framework adapters, native runtimes, and a DevTools overlay. 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](#ruby), [Python & Go](#python--go) below. ### Server SDK [#server] Use this on your backend (Node, Workers, Bun, Deno, Next.js Server Components, etc.). The server SDK polls the rule blobs in the background and evaluates locally — there is no per-request network call from your code. ```ts import { configure, Client } from "@shipeasy/sdk/server"; configure({ apiKey: process.env.SHIPEASY_SERVER_KEY ?? "", attributes: (u) => ({ user_id: u.id, plan: u.plan, country: u.country }), }); ``` `configure()` 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). The optional `attributes` transform maps your user object onto the Shipeasy attribute map used by every bound `Client`. If you need to override the env or base URL (rare — defaults are correct for almost everyone), pass `env` / `baseUrl` to `configure({ apiKey, env, baseUrl })`. #### Evaluating a feature flag ```ts const flags = new Client(currentUser); const enabled = flags.getFlag("new-checkout-flow"); ``` Bind a `Client` to the user once, then read with no per-call user argument. `getFlag()` is **synchronous** — the rule set lives in process memory. The attributes available to targeting rules come from the `attributes` transform you passed to `configure()` — see [User attributes](https://docs.shipeasy.ai/get-started/attributes) for the full guide. #### Reading a dynamic config ```ts const pricing = flags.getConfig<{ 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 ```ts 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 edge. ### Browser SDK [#browser] 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. ```ts import { configure, Client } from "@shipeasy/sdk/client"; configure({ clientKey: import.meta.env.VITE_SHIPEASY_CLIENT_KEY ?? "", attributes: (u) => ({ user_id: u.id, plan: u.plan }), }); const flags = new Client(currentUser); await flags.ready(); if (flags.getFlag("new-checkout-flow")) { // … } flags.track("checkout_viewed", { source: "nav" }); ``` #### Server-side rendering bootstrap For Next.js / Remix / SvelteKit, `configure()` once at startup, then bind a `Client` to the request user and read flags, configs and kill switches directly: ```tsx title="app/layout.tsx" import { configure, Client } from "@shipeasy/sdk/server"; configure({ apiKey: process.env.SHIPEASY_SERVER_KEY ?? "" }); const flags = new Client({ user_id: session?.user?.id }); if (flags.getFlag("new-checkout-flow")) { // render the new flow } ``` The server-rendered evaluation lands on `window.__SE_BOOTSTRAP` for the client SDK to pick up — no flicker. Configure the client with `configure({ clientKey })` and the bootstrap auto-hydrates before your first read. ### Framework usage [#frameworks] Shipeasy ships a single SDK — `@shipeasy/sdk` — that works from plain JavaScript. Every API (`getFlag`, `getConfig`, `getKillswitch`, `track`) is callable directly from React, Vue, Svelte, Angular, or vanilla JS without any framework-specific wrapper. ```tsx title="React" import { useEffect, useState } from "react"; import { configure, Client } from "@shipeasy/sdk/client"; configure({ clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY ?? "", attributes: (u) => ({ user_id: u.id }), }); const flags = new Client(currentUser); function CheckoutButton() { const [enabled, setEnabled] = useState(() => flags.getFlag("new-checkout-flow")); useEffect(() => { const onChange = () => setEnabled(flags.getFlag("new-checkout-flow")); window.addEventListener("se:override:change", onChange); return () => window.removeEventListener("se:override:change", onChange); }, []); return ; } ``` The browser SDK dispatches the `se:override:change` event whenever a re-poll updates the cached blob or the devtools overlay flips an override. Wrap that listener in whatever reactivity primitive your framework provides (Vue's `onMounted` + `onUnmounted`, Svelte's `onMount`, Angular's `ngOnInit`, etc.). #### A note on vanilla JS [#vanilla-js] Every Shipeasy SDK API works from plain JavaScript. You can drop the SDK into a plain ` ``` `k=` is your **public client key** (`sdk_client_…`) — safe to embed in HTML. `p=` is your project id; `boot.js` checks it against the key's project and returns `400` on a mismatch, so a tag assembled from two different projects' values fails loudly instead of silently serving the wrong one. > **Why the preamble mints a cookie** `__se_anon_id` is the stable id the visitor is bucketed on. It has to be minted in the page because the cookie is first-party to **your** domain and is never sent to our CDN — `boot.js` can only learn it from the URL. Without a stable id a visitor re-buckets on every navigation, so a half-rolled-out feature flickers on and off as they browse. ### Use the global Reads are synchronous. The data arrived with the script, so there is no `ready` promise to await: ```html ``` ### Identify the visitor Append `u=` for a known user and `attrs=` for anything your gate rules target on — plan, cohort, tenant: ``` https://cdn.shipeasy.ai/sdk/boot.js?p=&k=sdk_client_... &u=user-123 &a= &attrs=%7B%22plan%22%3A%22pro%22%7D ``` `attrs` is a URL-encoded JSON object. Country and the other geo attributes are filled in at the edge automatically, so you only need to pass what we can't derive. After a login you can re-evaluate without a page load: ```js await window.shipeasy.identify({ user_id: "user-123", plan: "pro" }); ``` ### When to use it (and when not to) - **No-build & static sites** — Plain HTML, a CMS template, a landing page, or a server-rendered app where you just want a flag check in a script tag. - **Bundled apps** — A React/Vue/Svelte app or any project with a bundler should install @shipeasy/sdk directly — you get types, tree-shaking, see() error reporting, and the full client API. The script tag is the **browser client** under a different delivery mechanism: it reads flags and configs for the identified visitor. For server-side evaluation, use the server SDK with your **server** key — never put a server key in a ` ``` Both attributes are **required**. If either is missing the script logs a clear `console.error` and exits without mounting anything. | Attribute | Where to find it | | --------------------- | ------------------------------------------------------------------- | | `data-client-api-key` | Dashboard → Settings → API Keys → Client key (public, safe in HTML) | | `data-project-id` | Dashboard → Settings → Project ID | **Drop the script tag into ** ```bash ``` Works in any HTML template regardless of backend language or framework. **Append ?se to any page** ```bash http://localhost:3000/?se=1 ``` The rail appears bottom-right. Toggle it any time with Shift+Alt+S. **Sign in once, then report** ```bash // In the overlay: File a bug / Request a feature. ``` First open prompts a one-time sign-in popup. The report lands in your dashboard within seconds. > **Note** The overlay is loaded lazily — the script tag itself adds no visible weight to normal page loads. It only activates when `?se=1` is in the URL or the user presses `Shift+Alt+S`. #### Next.js / TypeScript In a Next.js App Router root layout, read the keys from env: ```tsx // app/layout.tsx export default async function RootLayout({ children }) { return (