TypeScript SDK
The TypeScript SDK is the in-code half of smplkit: one client resolves flags, reads config, controls log levels, and emits audit events. You provision and inspect the underlying resources with the MCP tools or the Console.
Install
npm install @smplkit/sdkRequires Node.js 18+. The version installed is the latest published release.
Initialize the client
A single SmplClient exposes all four runtime products as sub-namespaces — client.flags, client.config, client.logging, and client.audit. The SDK key (sk_api_...) is read from SMPLKIT_API_KEY or the ~/.smplkit profile (or passed as apiKey); set environment once on the client so flag/config/logger resolution targets the right environment.
import { SmplClient } from "@smplkit/sdk";
const client = new SmplClient({
environment: "production",
service: "my-service",
});
// Recommended at startup: block until the cache is warm and the
// live-updates WebSocket is connected, so the first reads hit cache.
await client.waitUntilReady();
// ... do work ...
client.close(); // releases the WebSocket and stops background timersTypeScript has a single Promise-based client. Declaring handles and starting work are async (await); evaluating a flag with .get() and reading a bound config object are synchronous. Always close() the client on shutdown.
Flags
When you need a runtime on/off switch or a typed rollout value. Declare a typed handle with a safe default (the default is served when the flag is missing or smplkit is unreachable — pick the conservative value), attach context, then evaluate with .get().
import { SmplClient, Context } from "@smplkit/sdk";
// Declaring a handle is async — await it. Evaluation via .get() is sync.
const checkout = await client.flags.booleanFlag("checkout-v2", false);
const banner = await client.flags.stringFlag("banner-color", "red");
// Attach context. TypeScript supports both an ambient client.setContext([...])
// and a per-evaluation provider callback:
client.flags.setContextProvider(() => [
new Context("user", currentUser.email, { plan: currentUser.plan }),
new Context("account", String(currentAccount.id), { region: currentAccount.region }),
]);
if (checkout.get()) {
renderNewCheckout(); // .get() is synchronous — no await
}
// Per-call override bypasses the provider for one evaluation:
const colour = banner.get({
context: [new Context("user", "alice@acme.com", { plan: "enterprise" })],
});→ Full reference (every language, all context modes, change listeners): Flags Runtime
Config
When you need server-managed settings that update without a redeploy. Read a single value with getValue(id, key, default) (the three-arg form returns the default if the key is absent — it never throws), or bind() an object that the SDK mutates in place as values change.
// Single value, with a default that is served if the key is absent.
const slowQueryMs = await client.config.getValue(
"database",
"slow_query_threshold_ms",
500,
);
// bind() returns a live object — the SDK mutates it in place on every
// server-pushed change, so reads always reflect the latest values.
const billing = await client.config.bind("billing", {
plan: { max_seats: 5, trial_days: 14, tier: "free" },
});
console.log(billing.plan.max_seats); // reflects live overridesLogging
When you want to raise or lower log levels in production without redeploying. Call install() once — it auto-discovers winston and pino loggers, applies server-managed levels, and subscribes to live updates. After install, use your loggers normally; levels are server-driven.
const client = new SmplClient({ environment: "production", service: "my-service" });
// One consent-gate call hands all winston/pino loggers to smplkit.
await client.logging.install();
// Use winston / pino loggers as usual from here — levels are managed live.There is no console adapter — use a supported framework (winston or pino) to bring loggers under management.
Audit
When you need a durable, queryable record of who did what. Call client.audit.events.record(...) with a single camelCase options object. It is fire-and-forget by default (buffered and retried); pass flush: true for durability before a process exits or in tests.
await client.audit.events.record({
eventType: "invoice.created",
resourceType: "invoice", // must NOT start with "smpl." (reserved)
resourceId: "o-1",
actorLabel: "finance@example.com",
severity: "WARN",
data: { snapshot: { total_cents: 4900, currency: "USD" } },
flush: true, // or omit to flush asynchronously
});eventType, resourceType, and resourceId are required, and resourceType must not start with smpl. (reserved → 403).
The author + operate loop
Writing SDK code is only half of each change; the resource has to exist on the platform too. Write the SDK call, provision the resource with the MCP tools (create_flag / create_config / set_log_level), then verify (get_flag / get_config / query_events). The id used in code is the key you provision — they must match.
Example — ship a kill-switch for the new checkout:
// WRITE — in app code:
const checkout = await client.flags.booleanFlag("checkout-v2", false);
if (checkout.get()) {
renderNewCheckout();
}PROVISION — via MCP: create_flag(key="checkout-v2", type="boolean", default=false)
VERIFY — via MCP: get_flag(key="checkout-v2")The flag id in code (checkout-v2) equals the provisioned key.

