shipdit
v0.4.0
Published
shipd's typescript sdk
Readme
shipdit
TypeScript SDK for shipd feature flags.
Install
pnpm add shipditServer
Streaming requires a WebSocket implementation. Node.js 22+ provides one globally. On Node.js 20, supply a compatible constructor through the WebSocket option; otherwise, the client uses polling.
import { createClient } from 'shipdit';
import type { FlagDefinitions } from './shipd.types';
const client = createClient<FlagDefinitions>({
sdkKey: process.env.SHIPD_SDK_KEY!,
stream: true, // optional; requires Node.js 22+ or the WebSocket option
defaults: {
'new-checkout': false,
},
});
await client.ready();
if (client.isEnabled('new-checkout', { userId: user.id })) {
// ...
}
await client.close(); // flushes pending eventsOn a server client, identify() only records an identify event. Pass context to
each evaluation instead.
Browser
import { createClient } from 'shipdit/client';
const client = createClient({
sdkKey: 'shipd_sdk_client_...',
// persists last-good snapshot to localStorage by default
});
await client.ready();Evaluation never throws. If the snapshot is unavailable, configured defaults are returned.
Polling + realtime
- Initial snapshot over
GET /sdk/v1/snapshot(ETag-aware). - With
stream: true, a WebSocket towss://ws.useshipd.com/sdk/v1/streamforsnapshot.updated/snapshot.resyncinvalidation, then the same ETag HTTP fetch. - Polling continues as a fallback (
refreshIntervalMs, default 30s). SetrefreshIntervalMs: 0to disable polling after the initial fetch.
If the WebSocket drops, the SDK reconnects with exponential backoff + jitter
(1s → 2s → 4s → … → max 60s, reset only after an authenticated connected
frame) and keeps serving the last known snapshot while polling remains active.
Terminal auth failures use a slower bounded retry. Malformed or unsupported
server frames close the stream.
Every stream connection — browser and non-browser — requires a short-lived
ticket from POST /sdk/v1/stream-ticket. The SDK mints that ticket over HTTPS
with Authorization: Bearer before each connect and refreshes it when the
cached ticket is near expiry or after a terminal auth close. If minting fails,
polling stays active.
Browsers cannot set WebSocket headers. They authenticate the mint request with
the Bearer header, then connect with ?ticket=. Do not put the long-lived SDK
key in ?sdkKey=.
Version policy (Phase 4 — full resync only):
- Stream jump
10 → 15fetches the current snapshot (no assumed 11–14 replay) - After a notify, HTTP is retried briefly while
version < notified(KV lag) - HTTP bodies with
version < localare ignored (no rollback from delayed responses) - Unsupported
schemaVersionis rejected withUnsupported snapshot schema version N; SDK supports version 1and the last valid snapshot (or defaults) is kept - Malformed flags, rules, segments, or unsafe versions reject the entire snapshot (last-good / defaults stay)
- Browser cache never overwrites a newer in-memory / bootstrap snapshot
- Omit
streamto disable WebSocket and rely on polling only
Compatibility constants (also on VERSIONS):
| Constant | Meaning |
| ----------------------------- | --------------------------- |
| SNAPSHOT_SCHEMA_VERSION | Flag snapshot envelope |
| SDK_STREAM_PROTOCOL_VERSION | WebSocket frame protocol |
| SDK_PACKAGE_VERSION | Published shipdit package |
| CONTROL_PLANE_API_VERSION | Control-plane /api/v{n} |
The client fetches snapshots from https://edge.useshipd.com and, with
stream: true, opens a WebSocket to wss://ws.useshipd.com/sdk/v1/stream.
It obtains stream tickets over HTTPS from https://ws.useshipd.com. Local shipd development and tests can
point elsewhere with overrides. Never set unsafeEndpoint in production: it
receives your SDK key.
const client = createClient({
sdkKey: process.env.SHIPD_SDK_KEY!,
stream: true,
overrides: {
unsafeEndpoint: 'http://localhost:8787', // shipd development only
streamEndpoint: 'http://localhost:8100',
},
});Per-request HTTP fetches time out after fetchTimeoutMs (default 10s).
Events
Batched to POST /sdk/v1/events (evaluations, errors, unregistered flags, version skew, identify).
The edge accepts and logs them; identify events upsert evaluated_entities asynchronously.
Pass events: false to disable.
Identify
// Browser: sets the identity for later evaluations. Server: records an event only.
client.identify({ userId: 'user_123', traits: { plan: 'pro' } });
client.reset(); // logout — clears local identityReact
Bind hooks once in a client-only module. Typegen stays types-only so server
code can import FlagDefinitions without pulling React.
// src/lib/shipd.ts
import { createShipdReact } from 'shipdit/client/react';
import type { FlagDefinitions } from './generated/flags';
export const { ShipdProvider, useFlag, useVariant, useReady } = createShipdReact<FlagDefinitions>();import { createClient } from 'shipdit/client';
import { ShipdProvider, useFlag } from './lib/shipd';
const client = createClient<FlagDefinitions>({
sdkKey: 'shipd_sdk_client_...',
});
function Checkout() {
const enabled = useFlag('new-checkout');
return enabled ? <NewCheckout /> : <OldCheckout />;
}
export function App() {
return (
<ShipdProvider client={client}>
<Checkout />
</ShipdProvider>
);
}useFlag / useVariant / useNumber / useJson re-render when the snapshot
updates or identify / reset changes the evaluation context.
