@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/sdkCreate a client
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.
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:
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:
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
| Method | Returns | Description |
|---|---|---|
| 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) | () => void | Fires after a config change; returns an unsubscribe function. |
| ready() | Promise<void> | Resolves after the first snapshot fetch settles. Never rejects. |
| close() | void | Stops polling and drops all listeners. |
Config options
| Option | Default | Description |
|---|---|---|
| key | required | Your publishable SDK key (sos_sdk_*). |
| baseUrl | https://edge.shipos.app | The edge evaluate endpoint. |
| refreshInterval | 30000 | Snapshot 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. |
| fetch | global fetch | Custom fetch implementation. |
| onError | undefined | (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.