leftspin
leftspin/docsv0.1

Build a durable agent
in an afternoon.

leftspin replaces the backend you'd otherwise build for an AI feature: session exchange, model calls, durable workflows, per-user memory, approvals, artifacts, metering and audit. This page is the whole happy path.

TypeScript SDKSSEREST fallbackExpo / RN
Hosted runtime

The local sandbox runs today. Hosted keys are going to design partners first, and the surface below may change before GA. We version breaking changes and tell you before they land.

01

Install

One package for sessions, runs, streaming, memory, approvals, artifacts, usage and the ledger. It works in Expo/React Native and any JavaScript runtime with fetch. The HTTP surface is documented for native clients.

terminal
npm i @leftspin/sdk
npx leftspin init

or start the whole runtime locally — postgres, object storage, api, worker

terminal
npx leftspin sandbox   # api :8080 · worker · postgres · minio
02

Exchange a session

A publishable key identifies the app, never the user. Your auth decides who the user is; leftspin mints a short-lived JWT scoped to that app and user. The secret key stays on your server and is never accepted from a client.

session.ts
import { leftspin } from "@leftspin/sdk";

const app = leftspin({ publishableKey: "pk_test_…" });

// Your auth decides who the user is.
const session = await app.sessions.create({
  userId: "u_2f81c",
  ttl: "1h",
});

app.setSession(session.token); // held in memory, never on disk

Never ship sk_ keys in an app bundle. The SDK refuses them outside a server environment, and the acceptance suite greps the client for key material.

03

Start a run

Runs are rows, not processes. Creating one returns immediately; the workflow executes server-side and journals every step. The client owns a stream, never the work.

studio.tsx
const run = await app.runs.create({
  workflow: "studio_job",
  input: { prompt: "product shot, dusk", frames: 2 },
});

for await (const event of run.events()) {
  if (event.type === "progress") setProgress(event.pct);
  if (event.type === "artifact") setFrames((f) => [...f, event.url]);
  if (event.type === "needs_input") setApproval(event.prompt);
  if (event.type === "done") setStatus("done");
}
04

Stream, then reconnect

Every event has a per-run sequence id, so a reconnect is a seek rather than a new stream. Pass after or Last-Event-ID and the server replays from that offset.

terminal
curl -N "$LEFTSPIN_URL/v1/runs/$RUN_ID/events" \
  -H "Authorization: Bearer $USER_JWT" \
  -H "Last-Event-ID: 128"

Chat deltas are committed to the step result as well as streamed, so the journal and the stream always agree. A client dedupes on (seq, type) — it cannot double-apply a token.

05

Steps, approvals, effects

A workflow is a re-entrant function over the journal. On resume, completed steps return their recorded result and their bodies are skipped. Timers and approvals are rows, so waiting costs no worker.

workflows/studio.ts
export const studioJob = app.workflow("studio_job", async (ctx, input) => {
  const plan = await ctx.step("plan", () => planRender(input));

  const frames = await Promise.all(
    plan.frames.map((frame, i) =>
      ctx.step(`generate.${i}`, () => render(frame)),
    ),
  );

  const decision = await ctx.awaitApproval("publish", {
    summary: `${frames.length} renders ready`,
    timeout: "24h",
  });
  if (!decision.approved) return { cancelled: true };

  return ctx.sideEffect("publish", `studio:${ctx.runId}`, () =>
    publishAll(frames),
  );
});
primitiveguarantee
ctx.stepRuns at most once per (run, name). Result committed first.
ctx.sleepTimer is a row. A restart does not reset it.
ctx.awaitApprovalSuspends without a thread; resumes on a decided row.
ctx.sideEffectOne idempotency key per app. Applied once.
ctx.emitAppend-only event with a sequence offset.
06

Prove isolation

Every read and write is scoped by (app_id, user_id) from the JWT, and Postgres FORCE ROW LEVEL SECURITY rejects rows outside the transaction's scope. The ledger is append-only and hash-chained; verification recomputes it from genesis.

terminal
GET /v1/ledger?verify=1
Authorization: Bearer $USER_JWT

{
  "valid": true,
  "checked": "all entries",
  "entries": [
    { "seq": 1283, "action": "approval.granted" },
    { "seq": 1284, "action": "side_effect.applied" }
  ]
}

A cross-tenant request returns 404, not 403 — the API does not leak whether the resource exists.

07

Emit a conversion signal

Runtime events come from workflow milestones and are emitted in the same transaction as the work. Clients can send completion events; both share a dedup key so ad platforms see one conversion, not two.

terminal
curl -X POST "$LEFTSPIN_URL/v1/events" \
  -H "Authorization: Bearer $USER_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "trial_started",
    "dedup_key": "u_2f81c:trial:2026-11"
  }'

Delivery is an outbox: retries with backoff, dead-lettering, and a receipt per destination. Destinations include Meta CAPI, Google/Firebase, a generic webhook and a SKAN postback hook.

08

Errors

statuscodewhat it means
402quota_exceededMonth-to-date spend or the per-user daily cap was hit. Render a paywall.
404not_foundThe resource is outside the caller's (app, user) scope. Existence is not leaked.
409duplicate_signalThis dedup key was already recorded. Delivery is a no-op.
429rate_limitedPer-app or per-user limit reached. Retry with backoff.
503provider_unavailableAn upstream provider failed after retries. The run is requeued or quarantined, never half-applied.
09

Hosted limits

  • –The hosted runtime runs in a single region; BYOC and regional data planes are portfolio terms.
  • –Push is an inbox + local notification in the sandbox; APNs/FCM delivery is wired on the hosted path.
  • –LLM and media adapters ship for OpenRouter and fal; offline runs use a deterministic mock provider.
  • –The dashboard UI is rolling out; the API, CLI and MCP surface are live today.
  • –Token chunks are journaled per delta in the sandbox. v1 batches them and journals at step boundaries.
10

What leftspin is not

An agent framework

It runs your workflows. Your agent loop stays yours.

An MMP

Attribution stays with your measurement stack.

A generic BaaS

No auth UI, no CRUD builder, no dashboard product.

A token reseller

Model spend passes through. We charge for the runtime.

Ready to run it?

Run it locally, then move to managed when the first user does.