@wagleflags/sdk
v0.1.0
Published
Wagle feature-flag SDK for TypeScript and React: seed once, stream updates, evaluate flags locally with zero per-eval network hop.
Maintainers
Readme
@wagleflags/sdk: Wagle TypeScript & React SDK
The official TypeScript SDK for Wagle feature flags: seed once from
SyncService.Get, stay fresh over the WatchFlags server-stream, and evaluate flags locally
with zero per-eval network hop. Three entry points, one package:
@wagleflags/sdk: backend-trust client (Node / server-side)@wagleflags/sdk/frontend: browser-trust client (untrusted bundles)@wagleflags/sdk/react: React hooks over either client
The evaluation (xxHash64 bucketing, rule ordering, well-defined errors) is a byte-identical
mirror of the server's engine, pinned by shared fairness vectors in CI
(src/fairness.test.ts), so the SDK and the server always agree.
Install
pnpm add @wagleflags/sdkThe package includes its compiled dist/, so installation requires no build step.
Trust class: backend vs frontend
This entry point (@wagleflags/sdk, createWagleClient) is the backend-trust SDK: trusted,
server-side, it receives every flag, server_only (not-public) flags included, so it can seed
and locally evaluate them with zero hops. Ship it server-side only.
The browser-trust twin is @wagleflags/sdk/frontend
(createWagleFrontendClient): same seed→WatchFlags→local-eval loop, sharing the eval/store/stream
internals verbatim, but a server_only flag never enters its store. Ship that into an
untrusted client bundle (web/mobile). The classes differ only in the credential's trust: a
frontend-class credential is delivered only public flags (the server gates server_only off the
credential's class claim on the DB-free hot path), and the frontend client adds a by-construction
store guard so a leaked one still can't strand its key/targeting in an untrusted store. The two
are distinct entry points on purpose: trust class is a package choice, never a per-call flag.
Frontend SDK cost: read before shipping to browsers
Every browser tab running @wagleflags/sdk/frontend is its own sync client: one Get seed plus a
long-lived WatchFlags stream (or a Get poll every 30s) per visitor. Sync traffic (and your
Wagle bill) then scales with your audience, not your infrastructure.
For anything beyond low-traffic or internal apps, the recommended shape is a proxy through your
own backend: run the backend SDK (@wagleflags/sdk in Node, or the Go SDK) inside
your API and serve the evaluated flags (or the config subset your UI needs) from an endpoint you
own. One Wagle stream per server instance, zero marginal Wagle cost per visitor, and your flag
identifiers stay out of the bundle. Use @wagleflags/sdk/frontend directly when the audience is small
(internal tools, dashboards) or live-in-the-browser toggles are worth the traffic.
Model
createWagleClientstarts a seed→pump loop:Getseeds the localLocalStore, then eachWatchFlagsdelta applies inconfig_versionorder; a gap/Resync/reconnect re-Gets. The store swaps whole-version and torn-free and is auseSyncExternalStoresource.evaluate(anduseEvaluate/useFlag) is a pure, local read of the held snapshot: no network.- If the server has streaming disabled (
WatchFlagsreturnsUnimplemented, e.g. a cost-optimized deploy), the loop falls back to pollingGeteverypollIntervalMs(default 30_000) instead of looping; the client stays fresh, just pull-based.
Quickstart (vanilla)
import { createWagleClient } from "@wagleflags/sdk";
const client = createWagleClient({
url: "https://api.wagle.sh",
// your PROJECT ID: an opaque UUID from the console, not the display name.
project: "<project-uuid>",
environment: "production",
token: process.env.WAGLE_API_TOKEN, // the SDK refresh credential
});
await client.ready(); // resolves after the first seed
const res = client.evaluate("new-checkout", { entityId: "user-42", attributes: { plan: "pro" } });
// res is a Trace ({ variationKey, value, reason, ... }) or an EvaluationError; narrow with isEvalError.
client.close(); // stop the pumpThe SDK builds its own Connect transport from url: no createConnectTransport in a quickstart.
token is the durable refresh credential from the console's
"SDK Tokens" card: the SDK exchanges it for short access tokens via
SdkTokenService.RefreshSdkToken and attaches them as a bearer, so revoking a token cuts the SDK
off within a token TTL. The frontend twin (@wagleflags/sdk/frontend) takes the same url/token,
with a frontend-class credential.
Quickstart (React)
React bindings live in the @wagleflags/sdk/react subpath (react is an optional peer dependency):
import { useFlag } from "@wagleflags/sdk/react";
function Checkout({ client }: { client: WagleClient }) {
// Re-renders live when a WatchFlags delta swaps the snapshot; evaluates locally.
const value = useFlag(client, "new-checkout", { entityId: "user-42", attributes: { plan: "pro" } });
return value === "true" ? <NewCheckout /> : <OldCheckout />;
}useFlag→ the served value string, orundefinedwhen it does not resolve (unseeded / not found).useEvaluate→ the fullTrace | EvaluationError(for the reason / bucket).
Options
url: the wagle endpoint (e.g.https://api.wagle.sh); builds the transport internally.token: the SDK refresh credential, auto-exchanged for short access tokens and sent as a bearer.transport: advanced escape hatch, a ready Connect transport (the in-memory stub, tests, a custom transport). Exactly one ofurl/transportis required (both or neither throws).backoffMinMs/backoffMaxMs: reconnect backoff bounds (default100ms..5s, doubling).pollIntervalMs: how often to re-Getwhen the server has streaming disabled and the client falls back to polling (default30_000). The freshness-vs-cost dial; each poll is oneGet.onError(err): surfaced on stream/seed failures before a reconnect (defaultconsole.warn).
getSnapshot() returns the current whole-version snapshot; subscribe(listener) is the
useSyncExternalStore shape; ready() resolves after the first seed.
Notes
- One client is one
(project, environment). server_onlyflags: the backend SDK (this entry point) receives them; the frontend SDK (@wagleflags/sdk/frontend) never does. They're absent from its local snapshot.- For tests/demos, pass an in-memory
createRouterTransport(...)backend astransport(the escape hatch): a stub that streamsWatchFlagsreflects a toggle live with zero per-eval hop.
