Quickstart
Install the SDK and CLI, bind a project, wire one init call, ship a flag at 0%, and ramp — in about five minutes.
Zero to a ramping flag in 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, experiments, and i18n — starts here, then branches. If you only read one page, read this one.
Ship a flag in five minutes
~5 minutes · no card requiredAdd the SDK and 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
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:
shipeasy use my-project # or run inside a repo that already has a bindingFull flow, including CI tokens, lives in Authenticate.
Configure once, use everywhere
The single configure() call boots flags, experiments, and i18n. 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.
Create a flag at 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).
Wrap code with getFlag
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
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 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:
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
}import shipeasy
shipeasy.configure(api_key=os.environ["SHIPEASY_SERVER_KEY"])
flags = shipeasy.Client(current_user)
if flags.get_flag("checkout-v2"):
...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
}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
endimport 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");import ai.shipeasy.configure
import ai.shipeasy.Client
configure(System.getenv("SHIPEASY_SERVER_KEY"))
val flags = Client(currentUser)
flags.getFlag("checkout-v2")use function Shipeasy\configure;
use Shipeasy\Client;
configure(getenv('SHIPEASY_SERVER_KEY'));
$flags = new Client($currentUser);
$enabled = $flags->getFlag('checkout-v2');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.
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.
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.
Targeting rules, per-condition rollouts, gradual ramps, and kill switches.
Read →Dynamic configConfigsTyped JSON values you change at runtime — limits, copy, thresholds — without a deploy.
Read →A/B testingExperimentsAllocation, variant groups, holdouts on the universe, and significance verdicts.
Read →Localizationi18nTranslatable strings managed in the same SDK — one key per entrypoint, no separate init.
Read →