Documentation Index
Fetch the complete documentation index at: https://smithers.sh/llms.txt
Use this file to discover all available pages before exploring further.
Gateway is Smithers’ headless control plane. Reach for it (instead of startServer()) when long-lived clients — bots, dashboards, schedulers — need to authenticate once, stream events over WebSocket, decide approvals, inject signals, and manage cron schedules across many registered workflows.
This page covers the multi-workflow Gateway control plane. The Hono-based surface is Serve Mode, exposed through createServeApp() and smithers up --serve; it is the single-workflow HTTP app and is separate from the Gateway WebSocket/RPC runtime.
Quick start
Operator Console
Gateway serves a built-in operator console at/console. It gives non-coding operators a same-origin surface for health, workflow launch, active runs, and pending approvals. The console uses the Gateway RPC API and accepts the same bearer tokens as programmatic clients.
Disable or move it when embedding Smithers behind another app:
Custom React UI
Gateway can also serve a browser React app from the same origin as the RPC and WebSocket API. Use this when a workflow needs a custom operator surface instead of the built-in console./. Workflow-level UI defaults to /workflows/<workflowKey>, so the example above serves the workflow UI at /workflows/deploy. Gateway generates the HTML shell, bundles the entry with Bun for the browser, serves the bundle from <mount>/__smithers_ui/client.js, and injects globalThis.__SMITHERS_GATEWAY_UI__ with the mount, workflow key, RPC path, WebSocket path, and props.
Use the browser client SDK directly:
| Package | Exports |
|---|---|
smithers-orchestrator/gateway-client | SmithersGatewayClient, SmithersGatewayConnection, GatewayRpcError, RPC frame/type-map types, GatewayUiBootConfig, SmithersGatewayClientOptions |
smithers-orchestrator/gateway-react | createGatewayReactRoot, SmithersGatewayContext, SmithersGatewayProvider, useSmithersGateway, useGatewayRpc, useGatewayActions, useGatewayRuns, useGatewayRun, useGatewayRunEvents, useGatewayNodeOutput, useGatewayApprovals, useGatewayWorkflows |
RPC methods (TOON)
health remains available as a utility RPC and GET /health is available without auth. The legacy method names are still accepted for compatibility (runs.create, runs.get, runs.list, runs.cancel, runs.rerun, runs.diff, frames.list, frames.get, attempts.list, attempts.get, workflows.list, approvals.list, approvals.decide, signals.send, cron.list, cron.add, cron.remove, cron.trigger, getDevToolsSnapshot, jumpToFrame, devtools.jumpToFrame, devtools.getNodeOutput, devtools.getNodeDiff), but new clients should use the v1 names above.
Scopes
* grants every scope. Exact method grants such as launchRun also work. Legacy wildcard method grants such as cron.* continue to match legacy method names; typed scopes are the contract to use for new integrations. Legacy ranked grants (read, execute, approve, admin) are accepted so older tokens keep working.
rewindRun (destructive rewind)
Rewinds a run to a prior frame and makes it resumable from that point.
This is destructive: it truncates frames, attempts, output rows, and
diff-cache entries beyond the target; reverts JJ sandboxes; marks the
run running again; and emits a TimeTravelJumped event so
streamDevTools subscribers rebaseline.
Caller identity is authorized per-request: the connection must have
run:admin scope and must also be the run owner (userId matches
ownerId) or have role: "admin". Scope alone never grants access.
The legacy aliases jumpToFrame and devtools.jumpToFrame route to
rewindRun.
Request:
JumpResult):
run.time_travel_jumped with
{ runId, fromFrameNo, toFrameNo, timestampMs, caller }.
Quota: 10 rewinds per run per caller per hour (default window). Exceeded
→ RateLimited.
Failure modes and HTTP status:
| Code | Meaning | HTTP |
|---|---|---|
InvalidRunId | runId fails /^[a-z0-9_-]{1,64}$/. | 400 |
InvalidFrameNo | frameNo is not a non-negative i32 integer. | 400 |
ConfirmationRequired | Caller omitted confirm: true. | 400 |
FrameOutOfRange | frameNo > latest frame, or run has no frames. | 400 |
Unauthorized | Caller is neither the run owner nor an admin (audit row still written). | 401 |
RunNotFound | runId does not exist. | 404 |
Busy | Another rewind is in flight for this run. | 409 |
RateLimited | Caller exceeded rewind quota (default 10/hour). | 429 |
UnsupportedSandbox | A sandbox cannot be reverted (missing / untrackable jjPointer). | 501 |
VcsError | A JJ revert call failed; DB/reconciler rolled back. | 500 |
RewindFailed | Rewind failed and rollback was partial; run marked needs_attention. | 500 |
_smithers_time_travel_audit with result ∈ { success, failed, partial, in_progress }.
An in-progress row is inserted before any mutation and updated in place
on completion; startup recovery flips any leftover in_progress rows to
partial.
Node output
getNodeOutput returns the DevTools Output-tab payload for a single task iteration:
Error codes
Gateway v1 RPC errors use stable code strings and HTTP status mappings:InvalidRunId, InvalidFrameNo, or ConfirmationRequired. Treat those as
legacy aliases for the matching v1 validation failure.
Versioned wire shapes
All DevTools wire types carryversion: 1.
DevToolsSnapshot (v1):
DevToolsDelta (v1):
DevToolsEvent (v1) — frames pushed over devtools.event:
snapshot event, then emits delta events
per frame. The server re-baselines (emits a full snapshot instead of a
delta) after 50 delta events, when a delta is larger than a fresh snapshot,
or when the gateway observes TimeTravelJumped for the run.
WebSocket protocol
Three frame types share the same socket:req—{ type: "req", id, method, params? }from client.res—{ type: "res", id, ok, payload?, error? }from server, correlated byid.event—{ type: "event", event, payload?, seq, stateVersion }server-pushed;seqis per connection,stateVersionis global.
connect.challenge ({ nonce, ts }). The client replies with a connect request carrying minProtocol, maxProtocol, client metadata, auth, and an optional subscribe: string[] to filter events by runId. The server returns a hello payload (protocol, features, policy.heartbeatMs, auth with sessionToken/role/scopes/userId, snapshot).
After connect, the gateway emits tick events every heartbeatMs. launchRun, submitApproval, submitSignal, and cronRun automatically subscribe the connection to the affected runId. Streamed event names: connect.challenge, tick, run.event, run.heartbeat, run.gap_resync, run.error, node.started, node.finished, node.failed, task.output, task.heartbeat, approval.requested, approval.decided, approval.auto_approved, run.time_travel_jumped, run.completed, cron.triggered, devtools.event.
For stateless callers, POST /rpc accepts the same body shape ({ id, method, params }) and returns the same ResponseFrame. Auth headers: Authorization: Bearer <token> or x-smithers-key: <token> (or trusted-proxy headers in trusted-proxy mode).
GatewayOptions
ctx.auth = { triggeredBy, role, scopes, createdAt }. <Approval> may further restrict decisions with allowedScopes and allowedUsers, which the gateway enforces before accepting submitApproval.
headersTimeout and requestTimeout are applied to the underlying Node HTTP server when gateway.listen() starts. Keep both below the corresponding reverse-proxy idle/read timeouts so slow clients are closed by Smithers first.
Notes
- Cron:
gateway.register(name, wf, { schedule })writes a cron row keyedgateway:<name>; the gateway polls between 1 s and 15 s (clamped fromheartbeatMs). Cron-fired runs getctx.auth.role = "system",triggeredBy = "cron:gateway",scopes = ["*"]. - JWT mode currently validates
alg=HS256, HMAC,iss,aud,exp,nbf. Scope claims may be arrays or space/comma-separated strings. - Trusted-proxy mode is only safe behind something you control (Cloudflare Access, internal API gateway) that strips and rewrites identity headers.
- DevTools streams re-baseline every 50 events or when a delta exceeds a fresh snapshot; over-capacity subscribers receive
BackpressureDisconnect.