Skip to content

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:

  1. 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.
  2. Often no home directory. The ~/.smplkit configuration 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 VarPurpose
SMPLKIT_API_KEYAccount-scoped API key (required)
SMPLKIT_ENVIRONMENTEnvironment the function runs in (e.g. production)
SMPLKIT_SERVICEService 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_KEY as a secret and the rest as vars. With the nodejs_compat flag, Workers exposes them on process.env and the SDK picks them up automatically; without it, pass them as constructor arguments from your env binding.

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:

python
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 returns

If 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.
python
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

ClientBackground machinery by defaultStateless modeWhat changes
AuditIn-memory buffer + background deliverybuffered offrecord() is one awaited/blocking POST; typed errors; flush()/close() no-ops
FlagsWebSocket + periodic flush on first live usestreaming offOne fetch on first use; local evaluation; refresh() polls
ConfigWebSocket on first live usestreaming offOne fetch-and-resolve on first use; refresh() polls
LoggingWebSocket + discovery timer on install()streaming offinstall() applies once, awaited; refresh() re-applies
JobsNoneAlready stateless
PlatformNoneAlready stateless
AccountNoneAlready 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:

typescript
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 ~/.smplkit step. 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's buffered: false is 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 on AsyncLocalStorage); on the edge entry, pass flag evaluation contexts explicitly per call or register a setContextProvider. The winston/pino logging adapters are also package-root only.

A complete Worker:

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