Browser SDK
The Browser SDK (@smplkit/browser) reads Smpl Flags and Smpl Config directly in the browser, authenticated with a publishable public key (sk_public_...). It evaluates targeting rules locally, streams live updates, and is read-only by construction — there is no create/update/delete surface, and a private sk_api_ key is rejected at construction.
Use it when the code runs in your users' browsers: feature gates in the UI, client-side settings, rollout values that drive rendering. For your own servers — and for the other two runtime products, logging and audit — use the server TypeScript SDK (@smplkit/sdk) with a private key. The runtime method names are shared between the two, so snippets port unchanged.
Install
npm install @smplkit/browserZero runtime dependencies. The core is ≤ 12 KB gzipped, the React bindings ≤ 3 KB more. Browser-first, and also runs under Node.js 18+ for server-side rendering and edge workers.
Create a public key
The Browser SDK only accepts public keys. Create one in the console:
- Go to API Keys in the platform sidebar.
- Click Create Public Key.
- Choose the environment the key serves — a public key reads exactly one environment.
- Name it, click Create, and copy the key. It starts with
sk_public_.
Public keys are read-only, scoped to that single environment, and can only read flag and config definitions — which is what makes them safe to embed in your HTML or JavaScript bundle. There is no environment option anywhere in the client; the environment is baked into the key.
Never put a private sk_api_ key in browser code. Anything shipped to the browser is visible to every visitor, so a private key there is a credential leak — the client refuses to construct with one. See API keys for the two key kinds.
Quickstart
import { SmplClient, Context } from "@smplkit/browser";
const smpl = new SmplClient({ apiKey: "sk_public_..." });
await smpl.waitUntilReady();
// Identify the user for targeting rules.
smpl.setContext([new Context("user", "u_1", { plan: "pro" })]);
// Flags: typed handles with a safe default; .get() is synchronous.
if (smpl.flags.booleanFlag("dark-mode", false).get()) {
document.body.classList.add("dark");
}
const banner = smpl.flags.stringFlag("banner-color", "red").get();
const pageSize = smpl.flags.numberFlag("results-per-page", 20).get();
const theme = smpl.flags.jsonFlag("theme", { accent: "blue" }).get();
// Config: subscribe() returns a live view of a config's resolved values,
// getValue() reads a single value (the default is returned if it's absent).
const settings = await smpl.config.subscribe("web-app");
console.log(settings["api_timeout"]); // always reflects the latest pushed value
const timeoutMs = await smpl.config.getValue("web-app", "api_timeout", 500);A few things to know:
- Handle factories are synchronous. Unlike the server TypeScript SDK,
flags.booleanFlag(...)returns a handle immediately — evaluation runs against the local cache.await-ing the factory still works, so server-SDK snippets port unchanged. setContextis sticky. A browser serves one user, so the context applies to every subsequent.get()until replaced — call it again on login or logout. Per-call overrides are available via.get({ context: [...] }).waitUntilReady()resolves as soon as definitions are available from any source — immediately when there is warm bootstrap data (see Bootstrap and SSR), otherwise after the first fetch.- Change listeners:
smpl.flags.onChange(cb)(oronChange(id, cb)for one flag) andsmpl.config.onChange(...)fire when definitions change.smpl.close()releases the connection and timers.
React
The React bindings live at @smplkit/browser/react. Wrap your tree in SmplProvider, then read flags and config with hooks — components re-render automatically when values change.
import { SmplProvider, useBooleanFlag, useConfig, useSmplReady } from "@smplkit/browser/react";
function App() {
return (
<SmplProvider options={{ apiKey: "sk_public_..." }}>
<Page />
</SmplProvider>
);
}
function Page() {
const ready = useSmplReady();
const darkMode = useBooleanFlag("dark-mode", false);
const settings = useConfig("web-app");
if (!ready) return <Skeleton />;
return <main className={darkMode ? "dark" : ""}>timeout: {String(settings.timeout)}</main>;
}The hooks:
useBooleanFlag(key, default),useStringFlag(key, default),useNumberFlag(key, default),useJsonFlag(key, default)— evaluate a flag and re-render when its value changes. Pass a stable (memoized or module-level) default touseJsonFlag.useConfig(key)— a config's resolved values as an object; returns an empty object until definitions load or when the config does not exist.useSmpl()— the underlying client, forsetContext()on login and similar.useSmplReady()—trueonce definitions have loaded from any source; render a loading state instead of defaults whilefalse, if flashing defaults matters to you.
SmplProvider either constructs a client from options (and closes it on unmount) or accepts an already-constructed client. React 18+ is an optional peer dependency — non-React consumers install nothing extra.
Live updates
Streaming is the default: the client holds a Server-Sent Events connection and re-fetches definitions whenever a change event arrives. Polling runs as an automatic safety net — slow (15 minutes) while the stream is healthy, tighter (60 seconds) while it is down, and paused entirely in hidden tabs and while offline. Set streaming: false if your network mangles long-lived connections; the client then just polls.
Bootstrap and SSR
The bootstrap option controls where the first evaluation's data comes from:
"localStorage"(the default) caches the last payload per key, so returning visitors evaluate real values immediately while the client revalidates in the background."none"opts out of storage entirely; the first evaluation waits for the first fetch (or serves defaults).- A server-rendered payload hydrates the client with data fetched on your server, so SSR pages render without a flash of default values:
// server: fetch the flag and config lists with the same public key
// and pass the JSON bodies down to the page
const smpl = new SmplClient({
apiKey: "sk_public_...",
bootstrap: { flags: flagsJson, configs: configsJson },
});What is public
Everything this SDK receives is readable by your end users. A public key ships flag keys, targeting rule logic, and flag values — plus config keys and values — for its environment to the browser, where anyone can inspect them. That is what makes local evaluation instant, and it is not avoidable. Descriptions, sources, and timestamps are scrubbed from public-key reads, but the rules and values themselves are world-readable.
So, plainly:
- Never put secrets in flag values or config values a public key can read. API tokens, signing keys, internal URLs — none of it belongs in an environment a public key serves.
- Don't encode sensitive business logic in targeting rules if its disclosure would hurt you — rule logic is visible to anyone who opens devtools.
- Server-side secrets belong behind the server SDKs with private keys.
Related
- TypeScript SDK — the server-side counterpart
- Flags Runtime
- Config Runtime
- API keys — private vs public key kinds
- Create and rotate API keys
- Contexts and context types

