Required Checks
Run these checks before promoting a workflow module:.github/workflows/ci.yml) runs these same checks on pull requests and pushes to main.
Persistence
Use one SQLite database file per deployment on durable storage, or run PostgreSQL for managed, multi-connection storage (see below); internal tables are created and migrated idempotently on startup either way. Recommended practices:- Back up the database file and its WAL files together.
- Keep
PRAGMA foreign_keys = ON; Smithers enables it during schema setup and relies on it for core run artifacts. - Keep run IDs stable across resume attempts.
- Use Gateway event-stream sequence numbers for reconnects: clients resume from the last seen
seq. - Avoid manually deleting internal rows; delete whole runs through supported administrative paths so dependent frames, node diffs, and audit rows stay consistent.
PostgreSQL and PGlite
openSmithersBackend(schemas, opts) resolves the backend for the local Gateway path (async): opts.backend, then SMITHERS_BACKEND=sqlite|pglite|postgres, then backend in .smithers/smithers.config.ts, defaulting fresh workspaces to PGlite under .smithers/pg/. A legacy smithers.db with run data and no .smithers/migrated.json marker fails boot with SMITHERS_MIGRATION_REQUIRED instead of silently opening an empty backend. smithers migrate copies the SQLite store into PGlite; smithers migrate --to postgres --url <pg-url> targets managed Postgres instead. Migration opens the source database read-only, copies tables in bounded batches, verifies per-table counts, writes .smithers/migrated.json, and keeps smithers.db as a rollback backup unless you pass --keep-sqlite=false. Backend choice is a boot/deployment or migration-diagnostic decision only, not a ps, inspect, or other run-control flag: once healthy, controllers use the same RPC/client API regardless of the store behind it.
createSmithersPostgres(schemas, opts) runs the same durable engine and crash-and-resume guarantees on PostgreSQL or embedded PGlite, via the SQL dialect seam in packages/db/src/dialect.js: point it at managed Postgres with { provider: "postgres", connectionString }, pass a node-postgres connection config with { provider: "postgres", connection }, or run an in-process PGlite with { provider: "pglite", dataDir }. PostgreSQL URLs share one process-local pool, bounded to 16 connections by default; set SMITHERS_POSTGRES_POOL_MAX or pass postgresPoolMax for a positive explicit bound. An acquire that waits past SMITHERS_POSTGRES_ACQUIRE_TIMEOUT_MS (10 seconds by default) fails with PG_POOL_SATURATED naming the cap and the knob instead of hanging. The async factory returns the same createSmithers API plus a close() teardown.
pg, @electric-sql/pglite, and @electric-sql/pglite-socket are optional, loaded only on this path; the synchronous bun:sqlite path needs none of them. On Postgres, use database-native backups and connection-pool sizing instead of the file-and-WAL guidance above.
Electric Cloud Sync
Cloud GUI replicas use@smthrs/electric-proxy in front of ElectricSQL, run only against managed PostgreSQL configured for logical replication: wal_level=logical, a publication covering every _smithers_* table plus workflow output tables, and a replication slot for Electric. PGlite can’t be a source: it runs wal_level=replica and can’t create logical replication slots.
The proxy is the public shape endpoint: it authenticates the same scoped Gateway tokens, maps run:read to read-only shape access, injects workspace/run/user predicates from the grant, strips Authorization before forwarding to Electric, and caps 60 shape opens/minute, 50 active shapes, and 4 MiB per frame. Writes skip Electric shapes: clients post to /v1/electric/write, the Gateway applies the RPC mutation under scope checks and returns the PostgreSQL txid, and TanStack DB holds optimistic state until Electric streams that txid back.
Access Control
Expose the Gateway only behind TLS, with scoped bearer grants for automation and short TTLs for human-triggered actions. Recommended scopes by client type:
Rotate token grants regularly; revoke them once a user, CI job, or integration no longer needs access.
If you terminate user sessions at a reverse proxy and run the Gateway in
mode: "trusted-proxy", you must also declare the transport-level trust boundary with auth.trustedProxies: the peer addresses, CIDR blocks, or "unix" socket the proxy connects from. Identity headers are honored only from those peers, anything else gets 403 UNTRUSTED_PROXY_PEER, and a Gateway configured for trusted-proxy mode without an enforceable boundary refuses to start. The peer is the transport peer (the last hop that opened the connection), never an X-Forwarded-For entry. Bind the listener so the proxy is the only thing that can reach it, and see Gateway auth for the full failure-mode table.
For multi-tenant deployments, see Control Plane for org, project, usage, and audit primitives.
Execution Boundary
Sandbox workers run isolated so untrusted workflow code can’t reach the Gateway database or host filesystem:- request and result bundles are written under the run sandbox directory
- bundle manifests are size-bounded
- patch and artifact paths are checked against path traversal
- produced diffs require review unless
autoAcceptDiffsis enabled - sandbox records and events are persisted for audit
allowNetwork, container images, environment variables, ports, volumes, and CPU/memory limits per worker, and verify your runtime (Docker, Kubernetes, etc.) actually enforces them before running untrusted code. For high-risk code generation, run sandbox workers in a separate account, namespace, or machine with no ambient production credentials.
Secrets
Never pass long-lived credentials through workflow input; prefer short-lived caller tokens, scoped environment injection at the worker boundary, or a secret manager mounted only into the process that needs it. Operational rules:- Do not store provider keys in SQLite rows, run input, task output, or event payloads.
- Redact logs before forwarding them to shared observability sinks.
- Split launch permissions from approval permissions for workflows that can write files, create pull requests, or deploy.
Cache Policy
Use cache policy keys deliberately:scope: "run"keeps reuse inside one run.scope: "workflow"shares reuse across runs of the same workflow.scope: "global"shares reuse across workflow names.ttlMsbounds staleness; expired cache rows are treated as misses and refreshed.versionshould change whenever prompt, model, provider, tool behavior, or output semantics change.
Audit Trail
For incident review, preserve:- Gateway access logs
- Smithers run events
- rows in
_smithers_time_travel_audit, which record workflow rewind/replay events - sandbox bundle metadata and review decisions
- approval decisions, notes, and actor IDs
- deployment version and workflow module revision
Release Checklist
Before a production release:- CI is green on typecheck, dependency checks, and tests.
- Database backups have been restored in a staging environment.
- Gateway tokens are scoped and have bounded TTLs.
- Trusted-proxy deployments declare
auth.trustedProxies, and a direct request bypassing the proxy is refused with403 UNTRUSTED_PROXY_PEER. - Sandbox runtime enforcement has been tested against the intended threat model.
- Approval paths have a named owner and a fallback owner (see Approval).
- Logs are retained long enough to investigate delayed workflow failures.