Novacula is declarative. You don’t tell an executor “start this process now”; you write a desired spec to the control plane, and the executor on the other side polls, reconciles, and reports back. This page covers the shape of a spec, the revision lifecycle, and how the UI surfaces the gap between desired and observed state.Documentation Index
Fetch the complete documentation index at: https://docs.novacula.io/llms.txt
Use this file to discover all available pages before exploring further.
The spec
Every node has a single serializedNodeSpec stored on the Node row in the control plane. It is the complete declaration of what the executor must produce — chain, network, node type, client + version per role, container images, storage volumes, optional config overrides, sidecars, resource requests, and the desired lifecycle state (running or stopped).
The control plane is the only writer. Mutations like Deploy a node, Edit configuration, Upgrade a node, and lifecycle actions all produce a new spec.
Revision
Each spec write bumps a revision counter on the row. The revision is the contract between control plane and executor:- The control plane attaches the latest revision to every spec it serves.
- The executor reports back the revision it has currently materialized in its
ObservedNodeState. - If
observed.revision < desired.revision, the node is drifted — the UI shows a “pending change” indicator.
Reconcile loop
The executor runs three concurrent loops (see Agent (bare-metal) and Operator (Kubernetes) for backend specifics):| Loop | Default cadence | Job |
|---|---|---|
| PollingClient | 5s | Long-poll the control plane for assigned specs |
| Reconciler | 5s | Diff desired vs observed; apply changes through the chain adapter |
| Reporter | 10s | Probe each process, write ObservedNodeState back |
Observed state
The reportedObservedNodeState is what the UI renders on the node detail page. Its shape mirrors the spec, with the addition of a per-process ObservedNodeStatus enum:
| Status | Meaning |
|---|---|
starting | Process is launching but not yet healthy |
running | Process is healthy and accepting peers |
syncing | Process is healthy and importing blocks |
stopped | Process is not running (matches desiredState: stopped) |
error | Process exited or failed health checks |
Detecting drift
Two flavors of drift the platform raises automatically:- Revision drift — observed revision is older than desired. Usually transient: the executor will pick it up on the next poll.
- Liveness drift — the executor hasn’t called
syncExecutorfor longer thanHEARTBEAT_TIMEOUT_MS(30s default). The executor’sstatusvirtual field flips tooffline, and the node appears as unreachable until the executor reconnects.
desiredState is running but observedStatus is stopped or error, the node_down alert rule fires — see Alert rules.
Why declarative
Three properties fall out of this design:- Crash-safe executors. An executor can restart, lose memory, even reinstall — on the next poll it sees the latest spec and reconciles. There is no in-memory state to corrupt.
- Outbound-only deployment. The control plane never initiates anything; the executor pulls. See Architecture.
- Auditable history. Every revision is a discrete write; the Events feed records who changed what and when.