Skip to main content
Last verified: 2026-02-13 | Commit scope: bc0fb41

HTTP Request Flow

Auth middleware: internal/api/middleware.go. Both JWT and API key requests are subject to scope enforcement via RequireScope(). JWT users without the required scope receive 403 Forbidden. The admin scope is backend-granted only — users cannot self-assign it via the API key creation endpoint. Rollout endpoints require admin scope.
See also: CLAUDE.md — Server Separation for endpoint lists and auth details.

Subscription Lifecycle

Subscriptions follow a two-phase creation flow: API creates with pending_payment, payment consumer activates.
See also: Domain Model — Subscription State Machine for the canonical state machine and status definitions.
Creation: POST /api/v1/subscriptions (internal/api/handler_subscription.go)
  1. Check for existing pending_payment matching (user, chain, nodeType, duration) — return if found (idempotent)
  2. Validate chain profile and node type
  3. Resolve provider, region, instance_type from chain profile + duration mapping
  4. Create subscription with status pending_payment, ExpiresAt placeholder, PaymentID null
  5. Return HTTP 201 (created) or HTTP 200 (existing returned)
Activation: internal/consumers/payment_consumer.go — see NATS Consumers below.

Temporal Workflow Execution

When telemetry is enabled, activities are instrumented via TracingInterceptor — each activity creates an OpenTelemetry span.

Provision Node Workflow

File: internal/workflows/provision.go Steps marked with ? are conditional based on provisioning input specs. Compensation (on any failure): compensate() runs DestroyNode (Terraform destroy + key deletion) and UpdateNodeState -> failed.

Key Activity Details

Migrate Node Workflow

File: internal/workflows/migrate.go
Compensation (compensateMigration): Only if new infrastructure exists — destroys new host, restores node to degraded state. Old host preserved if failure occurs before new infra is ready.

Terminate Node Workflow

File: internal/workflows/terminate.go
Best-effort: Continues even if steps fail. Partial failures accumulate in PartialFailures[]. Only final state update is critical.

Command Execution and Progress Monitoring

Async Command Flow

Commands run asynchronously to avoid blocking heartbeats during long operations (e.g., 130GB snapshot downloads).
  1. Orchestrator pushes command to Redis queue via SendConfigureCommand
  2. Agent receives command in heartbeat response, sends command_acknowledged event
  3. Agent adds command ID to running_command_ids in subsequent heartbeats
  4. Agent executes command, reports progress via command_progress events
  5. Agent sends command_completed/command_failed, removes from running_command_ids

Liveness-Based Waiting

WaitForCommandCompletion (internal/activities/provision.go) uses liveness checks instead of fixed timeouts:
  1. Verify command ID in agent’s running_command_ids (retry: 3 attempts, 100ms backoff)
  2. Read progress from Redis progress:{commandID} for Temporal heartbeats
  3. Detect stalls: StallCount >= StallMaxThreshold (20) -> fail
  4. On completion event -> return. On liveness failure -> grace window -> fail.

Stall Detection

Configuration: internal/defaults/defaults.go under defaults.Commands.*

Provisioning Input Framework

Chains declare what inputs they need via provisioning_inputs: in chain config YAML. The framework handles schema resolution, validation, storage, and resolution for workflow activities.

Architecture

Package: internal/provision/input/

Input Types

Secret Sources

Input Spec Definition

Declared in chain config YAML under provisioning_inputs::
Backward compatibility: Legacy keys: field auto-converts to InputSpec with type: secret, source: generated.

Sealed Box Encryption

File: internal/crypto/sealedbox.go User-provided secrets are client-side encrypted using NaCl sealed box (X25519 + XSalsa20-Poly1305):
Handler: internal/api/handler_crypto.goCryptoHandler.GetPublicKey()
See also: CLAUDE.md — Provisioning Input Framework for code-level conventions.

CAPTURE_KEY Protocol

Proto: proto/agent.protoCOMMAND_TYPE_CAPTURE_KEY (value 8), CaptureKeyPayload { key_name, key_path, key_type } After CONFIGURE, some chains generate keys on disk. The CaptureChainGeneratedKeys activity:
  1. Sends CAPTURE_KEY command to agent with key path and name
  2. Agent reads file, encrypts with DEK, returns encrypted_key in response metadata
  3. Agent zeros plaintext via crypto.ZeroBytes()
  4. Activity stores encrypted key in node_keys via keyRepo.Create()
Security: validateKeyPath() prevents path traversal (must be within ChainDataDir).
See also: docs/plans/capture-key-protocol.md for implementation design.

Upgrade Rollout Workflows

Binary and config upgrades use a three-level workflow hierarchy orchestrated by Temporal, with COMMAND_TYPE_UPGRADE (value 9) delivered through the existing command queue. This is separate from the CONFIGURE path — upgrades do not re-execute recipes or wipe chain data. Manifest loading: Rollouts can be created in auto mode (upgrade_id provided) or manual mode. In auto mode, the API handler loads an upgrade manifest from {ConfigDir()}/upgrades/{chain_profile_id}/{upgrade_id}.yaml via manifest.Reader, auto-populating binary details, state compatibility, config changes, and content hash. See Upgrade Rollout — Manifest Loader.

Workflow Hierarchy

RolloutGroupWorkflow

Thin orchestrator for multi-binary chains. Iterates components in declared order, launching one RolloutWorkflow per component with a health gate between components.
  1. For each component in component_order:
    • Launch RolloutWorkflow as child
    • Wait for child completion
    • On failure: dispatch based on failure_policy (partial_ok, rollback_all, manual)
    • Health gate: validate all upgraded nodes healthy before next component
  2. FinalizeGroup activity sets terminal status
Signals: pause, resume, cancel, rollback, skip

RolloutWorkflow

Signals: Strategies: rolling (batch_size at a time), canary (canary_size first, then rolling), all_at_once (single batch, for hard forks). Scaling: Uses continue-as-new after each batch, resetting event history. No node count limit. Progress tracked in DB, not Temporal state.

UpgradeNodeWorkflow

Compensation: The agent handles per-action compensation (reverse-order rollback of completed actions). If agent-side rollback succeeds, node returns to previous state. If rollback fails (FAILED_ROLLBACK), a critical incident is created.

Upgrade Activities

Activity Profiles

Agent Upgrade Execution

The agent executes upgrades via a three-layer architecture: All 8 actions run inside a single Temporal activity (SendUpgradeCommand + WaitForCommandCompletion). The executor heartbeats Temporal during each action for visibility. Action sequence: BackupCurrentStateAcquireArtifactVerifyArtifactStopNodeInstallArtifactWriteConfigs (conditional) → ReloadDaemonStartNode
See also: Extending — Adding a Runtime Adapter for how to add support for new runtimes.

NATS Events (Upgrade Rollout)

New subjects for upgrade rollout events:

Metrics Flow (Observation System)

Collector types: prometheus, http, script, otlp — chain-agnostic, configured in observation.yaml CEL policy evaluation (internal/observation/):
  • Programs compiled once (LoadPolicies()), reused across cycles
  • metric(m, "name") / has_metric(m, "name") — binary bindings
  • Ordered verdict evaluation (first match wins): critical > degraded > ok > unknown
  • Circuit breaker on Victoria Metrics reads (fail-fast on outage)
  • PromQL injection guard validates chain_profile_id against strict regex
NATS auth: NATS uses JWT operator mode with two accounts: AGENT (ops-agents) and CONTROL_PLANE (internal services). Ops-agents receive a signed user JWT from the agent-gateway on first connect. Internal services self-sign ephemeral user JWTs using account signing seeds from Vault. Port 4222 internal, 4223 external (TLS).
See also: NATS JWT Operator Mode for the full authentication architecture, credential flows, and operational procedures.
See also: Health and Incidents for how policy verdicts feed into the incident pipeline.

NATS Consumers

Payment Consumer

File: internal/consumers/payment_consumer.go Subscribes to payment.completed on NATS JetStream (durable consumer, manual ACK). Two-path activation:
  • Path 1 (preferred): Activates existing pending_payment subscriptions linked during POST /api/v1/payments
  • Path 2 (fallback): Creates subscriptions from payment line items (backward compatibility)
Error handling:

Idempotency Store

File: internal/consumers/idempotency_store.go Redis-backed (RedisIdempotencyStore): key idempotency:{key} with configurable TTL. NoopIdempotencyStore for testing. Configuration: See Environment Variables for the full list of PAYMENT_CONSUMER_* variables (enabled, URL, stream name, consumer name, ACK wait, max deliver).

Payment-to-Provisioning Flow

See also: Payment Service for the payment service architecture.