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

Domain Objects

Node

Location: internal/models/node.go Field Ownership: All state mutations flow through NodeHealthMachine (internal/health/machine.go), which enforces transitions, uses optimistic locking, and emits events via health_event_outbox. State values:
  • state: provisioning, syncing, healthy, degraded, down, maintenance, terminating, terminated, failed
  • application_health: unknown, ok, degraded, critical
Display state: DisplayState() computes a combined state for API responses — infrastructure states take precedence, then application_health, then sync_status. Invariants:
  • Node belongs to exactly one subscription
  • Infrastructure fields (provider, host_id, host_ip) are internal-only
  • Terminal states (terminated, failed) have no outgoing transitions
  • Terminal transitions auto-reset sync_status and application_health to unknown
  • Migration cooldown prevents rapid re-triggering (default: 1h)

Node State Machine

Transition table (defined in models.validTransitions): API: CanTransitionTo(target), IsTerminal(), ValidSourceStates(target). Force updates via ForceUpdateState() bypass validation for compensation.
See also: Health and Incidents for how transitions are triggered.

Subscription

Location: internal/models/subscription.go User-provided: chain_profile_id, node_type, duration. System-resolved: provider, region, instance_type (from chain profile + duration mapping). API responses hide system-resolved fields.

Subscription State Machine

Invariants:
  • Infrastructure fields resolved at creation time (not user-provided)
  • pending_payment subscriptions deleted after TTL (default 30m)
  • Expiration triggers 24h grace period before termination
  • Abandoned pending_payment subscriptions have payment_id = null
See also: Workflows — Subscription Lifecycle for execution flow.

Incident

Location: internal/models/incident.go Categories: Incident Status Lifecycle: Dedup: Partial unique index on (node_id, category) WHERE status NOT IN ('resolved', 'auto_resolved'). Upsert increments occurrence_count, updates last_seen_at. No foreign keys: Incidents are historical records that survive node/subscription deletion.
See also: Health and Incidents for the full incident pipeline.

User

Location: internal/models/user.go Identity combinations: Invariants: Wallet address stored checksummed (EIP-55), lookups case-insensitive (lowercase index). Email optional and unique when provided. external_auth_id unique when not null. Created via Clerk webhook.

API Key

Location: internal/models/apikey.go, internal/models/apikey_scope.go Scopes: nodes:read, nodes:write, subscriptions:read, subscriptions:write, chains:read, keys:export, payments:read, payments:write, api-keys:manage, * (wildcard) Business rules: At least one scope required. Max 50 active keys per user. Min 1h expiration. Soft-deleted for audit trail. Rotation is atomic (new key + old revoked in one transaction). Plaintext returned only on creation/rotation.

Node Keys

Location: migrations/001_initial_schema.sql:88 Encryption hierarchy:
User-provided secrets flow:
Invariants: Master key never leaves Vault. DEK in plaintext only in agent memory during operation. All keys deleted on subscription expiration.

Auth Nonce

Location: internal/models/auth.go — Single-use challenge for wallet verification. 64-char random, 5-min TTL.

Refresh Token

Location: internal/models/auth.go — SHA-256 hashed, 7-day TTL. Atomic rotation on refresh (old revoked, new created in one transaction). Multiple active tokens per user (different devices).

Rollout Group

Location: internal/models/rollout.go Coordinates multi-binary upgrades (e.g., ethereum-holesky: geth + lighthouse). Executes component rollouts in declared order. Group status values: pending, running, paused, completed, partial, failed, cancelled, rolled_back Concurrency: Partial unique index on (chain_profile_id) WHERE status IN ('pending', 'running', 'paused') — one active group per chain. Standalone single-component rollouts do not require a group.

Rollout

Location: internal/models/rollout.go Single-component upgrade rollout. Tracks progress, strategy, and per-node status. Rollout status values: pending, scheduled, running, paused, completed, failed, cancelled, rolled_back Concurrency: Partial unique index on (chain_profile_id) WHERE status IN ('pending', 'scheduled', 'running', 'paused').

Rollout Status State Machine

Rollout Node

Location: internal/models/rollout.go Per-node upgrade tracking within a rollout. Node upgrade status values: pending, in_progress, validating, succeeded, failed, rolled_back, skipped

UpgradePhase Enum

Display states derived from executor progress through the action sequence:

NodeConfigState

Location: internal/models/node_config_state.go Tracks binary version per node. Pre-existing node_config_state table now has Go model and repository. Written by UpdateNodeBinaryVersion activity after successful upgrade, and by provisioning workflow after initial CONFIGURE.
See also: Workflows — Upgrade Rollout Workflows for the full workflow hierarchy.

Business Rules

Key Management

  1. Keys exist in plaintext only temporarily for node operation
  2. Encrypted recovery backups only within subscription TTL
  3. No human-accessible key material (no SSH, keys only via encrypted gRPC)
  4. Recovery without backup is impossible by design
  5. User-provided secrets use client-side NaCl sealed box encryption

Migration

  1. Gradual detection: Consecutive failures tracked per node (default: 3 required for DOWN)
  2. Cooldown: 1h between migration attempts (persisted in DB)
  3. Auto-recovery: Heartbeat during DOWN grace period -> back to HEALTHY, counter reset
  4. Steps: Verify backup -> provision new infra -> restore keys -> start node -> destroy old
  5. Non-blocking: No user approval required

Subscription Lifecycle

  1. Two-phase creation: POST /subscriptions creates with pending_payment. Idempotent — returns existing if same user/chain/nodeType/duration.
  2. Pending cleanup: Abandoned pending_payment deleted after TTL (default 30m)
  3. Expiration: active -> expiring (24h grace period). Nodes continue running.
  4. Termination: Grace period expired -> terminate all nodes, delete backups -> terminated

Database Schema Summary

Main App Database

Payment Service Database

Separate PostgreSQL instance. See Payment Service for schema.

Key Indexes

State Storage Locations