ShipOS docs
@shipos/sdk Node + browser

JavaScript / TypeScript

@shipos/sdk is a zero-dependency client for Node and the browser. Every flag check takes a default and hands it back on any failure, offline, on a typo, or during an outage - so it never throws and never blocks your request path.

Install

npm i @shipos/sdk

Create a client

typescript
import { createClient } from "@shipos/sdk";

const shipos = createClient({ key: "sos_sdk_..." });

Checking a flag

flag() returns the value or your default. Flags can be any JSON type, boolean, string, number, or object.

typescript
const useV2 = await shipos.flag("checkout-v2", {
  userId: "u_42",
  attributes: { plan: "pro", region: "eu" },
  default: false,
});

const theme = await shipos.flag("theme", { userId, default: "light" });
const limit = await shipos.flag("rate-limit", { default: 100 });
const retry = await shipos.flag("retry-config", {
  default: { retries: 3 },
});

No try/catch

Because flag() never throws, you never wrap it in a try/catch. If anything goes wrong you get your default - that's the whole contract.

Node example

A route handler gating a response behind a flag:

app/api/checkout/route.tstypescript
import { createClient } from "@shipos/sdk";

const shipos = createClient({ key: process.env.SHIPOS_SDK_KEY! });

export async function POST(req: Request) {
  const { userId } = await req.json();

  const useV2 = await shipos.flag("checkout-v2", {
    userId,
    default: false,
  });

  return Response.json({ checkout: useV2 ? "v2" : "v1" });
}

It won't hold your process open

In Node the refresh timer is unref'd, so the background poll never keeps the process alive. Call close() if you want to stop it explicitly.

Browser example

Provide defaults for an instant, offline-safe first render, then live-update when config changes:

typescript
import { createClient } from "@shipos/sdk";

const shipos = createClient({
  key: "sos_sdk_...",
  defaults: { "new-nav": false, theme: "light" },
});

// Re-render whenever the config version changes.
const unsubscribe = shipos.on("update", () => {
  render();
});

async function render() {
  const newNav = await shipos.flag("new-nav", { default: false });
  document.body.classList.toggle("new-nav", newNav);
}

await shipos.ready();
render();

API methods

MethodReturnsDescription
flag(key, opts)Promise<T>The flag value, or opts.default on ANY failure. Never throws, no try/catch needed.
flagDetail(key, opts?)Promise<EvaluationResult>{ value, reason, variation, ruleId?, bucket? }, the value plus why you got it.
allFlags(context?)Promise<Record<string, JsonValue>>Every flag in the environment (client defaults, or {} on failure).
on("update", cb)() => voidFires after a config change; returns an unsubscribe function.
ready()Promise<void>Resolves after the first snapshot fetch settles. Never rejects.
close()voidStops polling and drops all listeners.

Config options

OptionDefaultDescription
keyrequiredYour publishable SDK key (sos_sdk_*).
baseUrlhttps://edge.shipos.appThe edge evaluate endpoint.
refreshInterval30000Snapshot poll cadence + eval-cache TTL, in ms. Clamped ≥5000; 0 disables polling.
defaults{}Record of offline fallbacks used before the first fetch and on failure.
fetchglobal fetchCustom fetch implementation.
onErrorundefined(err) => void, observe swallowed errors without changing control flow.

How it works

  • Evaluations POST to edge.shipos.app/v1/evaluate and are cached in-memory per (flag, userId, attributes) for refreshInterval.
  • A background poll hits GET /v1/snapshot with If-None-Match. A 304 is nearly free.
  • On a version change the cache clears and the "update" event fires, so your next check sees fresh values.