Serverless & Edge Environments
The smplkit SDKs run in serverless and edge runtimes — AWS Lambda, Google Cloud Functions, Azure Functions, Cloudflare Workers, Vercel and Netlify functions, and scale-to-zero containers. This guide covers what changes in those environments and how to configure each SDK for them.
Two properties of serverless runtimes matter to an SDK:
- No long-lived process. Background machinery — buffered writers, flush timers, worker threads, streaming WebSocket connections — assumes a process that sticks around. A function instance can be frozen or torn down the moment your handler returns, stranding whatever was still in a buffer.
- Often no home directory. The
~/.smplkitconfiguration file may not exist (and on edge isolates there is no filesystem at all), so configuration comes from environment variables or explicit constructor arguments.
Every smplkit client addresses both: configuration resolves cleanly without the file, and clients that install background machinery have a stateless mode that turns writes into one-shot, awaited calls with no timers, threads, or sockets.
Configuration without a home directory
The resolution order is the same everywhere: SDK defaults → ~/.smplkit file → SMPLKIT_* environment variables → constructor arguments. When there is no ~/.smplkit, the file step contributes nothing and resolution falls through to environment variables — no error, no special casing.
This applies uniformly to the top-level client and to every standalone product client (audit, config, flags, logging, jobs, platform, account, per SDK naming). If the top-level client can resolve a value, so can any standalone client.
Set these as environment variables in your function configuration:
| Env Var | Purpose |
|---|---|
SMPLKIT_API_KEY | Account-scoped API key (required) |
SMPLKIT_ENVIRONMENT | Environment the function runs in (e.g. production) |
SMPLKIT_SERVICE | Service name for discovery and flag evaluation context |
- AWS Lambda / Cloud Functions / Azure Functions — set them as function environment variables (use your platform's secret store for the API key).
- Cloudflare Workers — add
SMPLKIT_API_KEYas a secret and the rest as vars. With thenodejs_compatflag, Workers exposes them onprocess.envand the SDK picks them up automatically; without it, pass them as constructor arguments from yourenvbinding.
TIP
Constructor arguments always work, everywhere. If your platform hands you configuration some other way (like the Workers env object), pass apiKey, environment, and service explicitly and skip the environment variables entirely.
Stateless client modes
Each product client falls into one of three groups.
Already stateless: jobs, platform, account
The jobs, platform, and account clients are pure request/response CRUD. They install no buffers, timers, threads, or sockets in any SDK — construct them anywhere, including per invocation, with no special options.
Audit: turn off the buffer
By default, record() enqueues onto an in-memory buffer and returns immediately; a background worker delivers events with retry. That is the right shape for servers and the wrong shape for functions — a frozen instance can strand buffered events.
In serverless environments, construct the audit client with buffering off. Every record() becomes one awaited/blocking POST that either succeeds or raises the SDK's typed errors, and flush()/close() become no-ops because nothing ever pends:
from smplkit import AuditClient
audit = AuditClient(buffered=False)
audit.events.record(
event_type="invoice.created",
resource_type="invoice",
resource_id="inv-1",
) # durable when this returnsIf you keep the default buffered mode in a function (for hot paths that record many events per invocation), call flush() before your handler returns so nothing is stranded.
Config and flags: poll instead of stream
On servers, the config and flags runtime surfaces open a WebSocket on first use so changes stream in live. A function instance can't host that connection. Construct the client with streaming off instead:
- The first live call fetches everything once (awaited) — flag definitions, or every config resolved for your environment.
- Reads and flag evaluations stay local and fast.
refresh()re-fetches on demand; change listeners fire from refresh deltas.- No socket, timers, or background state are ever created.
from smplkit import FlagsClient
flags = FlagsClient(streaming=False)
beta = flags.boolean_flag("beta-checkout", default=False)
if beta.get():
...
# later, at a cadence you control:
flags.refresh()The config client takes the same option and behaves the same way: subscribe/get_value resolve from one fetch, and refresh() picks up changes.
Know what you're trading
With streaming off, a flag or config change reaches an instance only when that instance calls refresh() (or is replaced). If you cache a client across invocations, decide on a refresh cadence — per invocation for strict freshness, or on an interval you track yourself for throughput.
Logging: apply once
The logging client's install() hooks your logging framework, applies the server's levels, and — by default — opens a socket plus a periodic discovery timer for live updates. With streaming off, install() still loads adapters, flushes discovery, and applies levels, all awaited — but creates no socket and no timer. Call refresh() to re-fetch and re-apply on demand.
In most serverless setups the logging management surface (logger and log-group CRUD) is the useful part, and it needs no install() at all.
What "stateless" means per client
| Client | Background machinery by default | Stateless mode | What changes |
|---|---|---|---|
| Audit | In-memory buffer + background delivery | buffered off | record() is one awaited/blocking POST; typed errors; flush()/close() no-ops |
| Flags | WebSocket + periodic flush on first live use | streaming off | One fetch on first use; local evaluation; refresh() polls |
| Config | WebSocket on first live use | streaming off | One fetch-and-resolve on first use; refresh() polls |
| Logging | WebSocket + discovery timer on install() | streaming off | install() applies once, awaited; refresh() re-applies |
| Jobs | None | — | Already stateless |
| Platform | None | — | Already stateless |
| Account | None | — | Already stateless |
Option naming per SDK: Python and Ruby take buffered/streaming keyword arguments; TypeScript and C# take them as constructor options; Java as builder methods (.buffered(false), .streaming(false)); Go inverts them as Config fields with safe zero values (DisableEventBuffering, DisableStreaming). In Go the fields also apply when you construct the top-level client — pair them with DisableTelemetry: true there to keep the metrics flusher off too.
TypeScript edge entries
Edge runtimes like Cloudflare Workers bundle your code and reject Node built-ins. The TypeScript SDK ships a dedicated subpath entry per product whose import graph is free of Node built-ins and of the ws package:
import { AuditClient } from "@smplkit/sdk/audit";
import { ConfigClient } from "@smplkit/sdk/config";
import { FlagsClient } from "@smplkit/sdk/flags";
import { LoggingClient } from "@smplkit/sdk/logging";
import { JobsClient } from "@smplkit/sdk/jobs";
import { PlatformClient } from "@smplkit/sdk/platform";
import { AccountClient } from "@smplkit/sdk/account";Each entry exposes the same client and the full typed error surface. Three differences from the package root:
- No
~/.smplkitstep. Configuration resolves defaults →SMPLKIT_*environment variables → constructor options. (An isolate has no home directory anyway.) - No WebSocket. Construct config/flags/logging clients with
streaming: false; leaving streaming on throws a clear error on the first live call. Audit'sbuffered: falseis unrelated to sockets but is the right pairing on Workers, where timers can't be created at module scope. - No ambient request context.
client.setContext(...)is a package-root affordance (it rides onAsyncLocalStorage); on the edge entry, pass flag evaluation contexts explicitly per call or register asetContextProvider. The winston/pino logging adapters are also package-root only.
A complete Worker:
import { AuditClient } from "@smplkit/sdk/audit";
import { FlagsClient } from "@smplkit/sdk/flags";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const flags = new FlagsClient({
apiKey: env.SMPLKIT_API_KEY,
environment: "production",
streaming: false,
});
const audit = new AuditClient({
apiKey: env.SMPLKIT_API_KEY,
environment: "production",
buffered: false,
});
const newFlow = await flags.booleanFlag("new-checkout-flow", false);
await audit.events.record({
eventType: "checkout.started",
resourceType: "cart",
resourceId: new URL(request.url).searchParams.get("cart") ?? "unknown",
});
return new Response(newFlow.get() ? "new flow" : "old flow");
},
};For other languages there is no bundling step to worry about — the standard package works in Lambda-style runtimes as-is; just use the stateless options above.
Related
- Configuration — the full resolution order, file format, and profiles
- SDK quickstarts — per-language install and initialization
- Emit audit events — the audit recording surface in depth

