Architecture overview
Included from Basic tier
OpsChain is one statically-linked binary (opschain) that is
simultaneously the CLI, a local execution engine, the server and the
runner agent. The entrypoint selects what gets wired; the engine inside
is the same everywhere:
opschain <resource> <verb> … # the CLI, pointed at a server (or local)
opschain run <action> | workflow <code> … # local execution against a working copy (the alias of runs create … --local)
opschain server start # API + scheduler + embedded GUI + store
opschain runner # execution client: register, poll, run
Because every surface ships in the same binary, everything is
release-aligned by construction: the reference documentation
(opschain docs, /api/docs), the getting-started guide
(opschain learn), the OpenAPI document, the GUI and the embedded
database migrations all describe exactly the build that is running.
Ports and adapters — one engine, mode parity
The engine (changes, workflows, step trees, the action catalog, properties/settings, authorisation) is written once against port interfaces — store, queue, dispatch, executor, log pipeline, blobstore, secrets, identity, git. Each deployment mode swaps adapters, never engine code, so a change or workflow produces the same step tree, the same semantics and the same logs in local mode and against a server. That parity is structural: the engine packages import only ports, and the executor conformance suite runs identically against every wiring.
| Concern | Local mode | Server mode |
|---|---|---|
| Store | SQLite file in the OpsChain home | PostgreSQL (or SQLite for embedded installs) |
| Dispatch | in-process | store-backed lease queue → registered runners |
| Executor | hostexec / containerexec | chosen per runner, same contract |
| Identity | single user | local users, tokens, directory providers |
Storage
Persistent state lives in two places: the data store (PostgreSQL or
an embedded SQLite file, selected by the shape of the store config
key) and the blobstore (a content-addressed directory holding bulk
bytes — log segments and artifacts — so they never sit in the OLTP
path). Migrations are embedded and run at server start. See
storage.
The server and the dispatch plane
opschain server start composes one process: the API (the /api
contract the CLI and GUI consume), the scheduler, the runner and agent
RPC planes, and the embedded GUI. Ready steps land in a store-backed
lease queue; the dispatch plane orders them by priority lane and
fair-share, checks placement against the runner registry, and annotates
every queued entry with a satisfiability verdict so unsatisfiable work
reads as queued-with-a-reason, never a silent hang. See
dispatch and fleet-queue.
CLI / GUI ──HTTPS──▶ ┌───────────────────────────────┐
│ opschain server │
│ API ─ engine ─ scheduler │ ┌────────────┐
│ embedded GUI │ │◀────│ opschain │
│ ▼ │ long│ runner │
│ store (PG/SQLite) blobstore │ poll│ executor │
└───────────────────────────────┘ └────────────┘
Runners and executors
A runner is an outbound-only execution client: it registers once with a
registration token, then long-polls the server for leased work —
runners dial in; the server never connects to a runner. Each runner
holds one job at a time (up to runner.max_parallel_steps jobs of one
pinned run, see dispatch), executes each through the
executor contract
(step_context.json in, step_result.json out, masked logs streamed
back) as a host process (hostexec) or a per-step container
(containerexec), so capacity scales by running more runners. See
runner, runner-registry and
runner-executors.
The step tree, the event spine and chains
Every change and workflow run compiles into a step tree — stages,
changes, waits and fan-outs — and the tree is the execution record:
entry verdicts, retries, outputs and skips are recorded on the steps
themselves. Every run and step transition, every supervised verb and
every plane lifecycle fact lands on the append-only event stream,
each event carrying a correlation_id and chain_depth, so any event
is a handle into its whole cascade; triggers match that stream and
create runs in response, extending the same chain. See
orchestration, events and
triggers.
Actions and packs
Work is authored as YAML actions in a git repository (templates pin
exact revisions per asset), composed with typed uses: + with:
parameters. Action packs are git repositories of reusable actions —
script:execute, terraform, ansible — mounted by name and called like
any other action. See opschain docs getting-started/2-1-your-first-action and getting-started/2-4-using-packs.
The embedded GUI
The web GUI ships inside the binary and is served by the server: the
estate, runs and activity views, the Operations area (fleet, licence,
maintenance, identity, configuration — see
gui-operations), and a Documentation section
reading this embedded documentation set from /api/docs.
High availability
Several server replicas run against one PostgreSQL database and one
shared blobstore behind any load balancer that honours GET /readyz
(no session affinity required): replica heartbeats, shard ownership and
a leader lease for cron and maintenance live in the shared store; every
run is driven by exactly one replica under a durable lease that a peer
takes over when the driver dies; and every step attempt's log is spooled
by exactly one replica — the first to receive a chunk of it — with the
other replicas forwarding the attempt's chunks there over the
replica-to-replica lane (peer_url) and the owner sealing the log into
the blobstore when the attempt ends. The operations guide is
opschain learn 8.7.
Related
- Concepts glossary: concepts.
- Runbooks:
opschain docs getting-started/00-README— the learning path.