Skip to content

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

bash
npm install @smplkit/sdk

Requires 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.

typescript
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 timers

TypeScript 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().

typescript
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.

typescript
// 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 overrides

Config Runtime

Logging

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.

typescript
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.

Logging Runtime

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.

typescript
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).

Emit audit events

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:

typescript
// WRITE — in app code:
const checkout = await client.flags.booleanFlag("checkout-v2", false);
if (checkout.get()) {
  renderNewCheckout();
}
text
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.