archy/.planning/codebase/ARCHITECTURE.md
2026-07-29 10:25:54 -04:00

21 KiB

Architecture

Analysis Date: 2026-07-29

System Overview

┌────────────────────────────────────────────────────────────────┐
│                      Frontend Layer (Vue 3)                     │
│              `neode-ui/src` (TypeScript + SPA)                  │
│  Routes → Views → Components → Composables → RPC Client        │
└────────────────┬─────────────────────────────────────────────┘
                 │ WebSocket + HTTP(S)
                 │ JSON-RPC 2.0 protocol
                 ▼
┌────────────────────────────────────────────────────────────────┐
│                    HTTP Server Layer (Hyper)                    │
│          `core/archipelago/src/server.rs`                       │
│  TCP Listener → Hyper → Router → ApiHandler/RpcHandler         │
└────────────────┬─────────────────────────────────────────────┘
                 │
      ┌──────────┴──────────┬──────────────────┐
      │                     │                  │
      ▼                     ▼                  ▼
  ┌─────────┐        ┌──────────┐      ┌────────────┐
  │  WebSocket       │   RPC    │      │   Content  │
  │  Handler         │  Handler │      │   Proxy    │
  │ (state sync)     │ (methods)│      │ (app URIs) │
  └─────────┘        └──────────┘      └────────────┘
      │                  │
      └──────────┬───────┘
                 │
                 ▼
    ┌─────────────────────────────────────┐
    │     Service Layer (Async Tasks)     │
    │ `core/archipelago/src/api/rpc/*`    │
    │                                     │
    │ • auth, identity, secrets           │
    │ • container orchestration           │
    │ • bitcoin, lightning, wallet        │
    │ • mesh, federation, FIPS            │
    │ • content, backup, settings         │
    └─────────────┬───────────────────────┘
                  │
      ┌───────────┼───────────┬──────────────┐
      │           │           │              │
      ▼           ▼           ▼              ▼
  ┌─────────┐ ┌──────────┐ ┌────────┐ ┌──────────┐
  │Container│ │ State    │ │BlobStore
  │Orch.    │ │Manager   │ │        │ │Identity  │
  │(Podman) │ │(Broadcast
  │         │ │ channels)│ │ ContentClient Manager │
  └─────────┘ └──────────┘ └────────┘ └──────────┘
      │           │           │         │
      └───────────┼───────────┼─────────┘
                  │
                  ▼
    ┌─────────────────────────────────────┐
    │     Persistent Storage Layer        │
    │                                     │
    │ • Data directory files (YAML/JSON)  │
    │ • SQLite (session store)            │
    │ • Blob store (content-addressed)    │
    │ • Podman container state            │
    │ • Secret vaults (encrypted)         │
    └─────────────────────────────────────┘

Component Responsibilities

Component Responsibility File
Server HTTP listener, connection multiplexing, TLS/encryption core/archipelago/src/server.rs
ApiHandler HTTP request routing, authentication, response formatting core/archipelago/src/api/handler/mod.rs
RpcHandler JSON-RPC 2.0 dispatch, method registration, rate limiting core/archipelago/src/api/rpc/mod.rs
ContainerOrchestrator Podman lifecycle, manifest reconciliation, adoption core/archipelago/src/container/prod_orchestrator.rs
StateManager Central state broadcast channel, revision tracking core/archipelago/src/state.rs
AuthManager User credentials, session validation, password hashing core/archipelago/src/auth.rs
Identity Manager Node Ed25519 keys, seed derivation, Tor address core/archipelago/src/identity_manager.rs
BootReconciler Periodic manifest sync loop, adoption, remediation core/archipelago/src/container/boot_reconciler.rs
Frontend Router Vue Router, page navigation, deep linking neode-ui/src/router/index.ts
Frontend Stores Pinia state (apps, settings, user, mesh) neode-ui/src/stores/
Frontend Components UI elements, modals, cards, layout primitives neode-ui/src/components/

Pattern Overview

Overall: Multi-tier async architecture with centralized request dispatch and broadcast state synchronization.

Key Characteristics:

  • Async-first (Tokio) - All I/O operations are non-blocking; task spawning for background work
  • RPC-driven API - Frontend communicates via JSON-RPC 2.0 (not REST); single /api/v0 WebSocket + HTTP endpoint
  • State as broadcast - Global state changes flow through Tokio broadcast channels to all connected WebSocket clients
  • Manifest-driven containers - App lifecycle controlled by declarative YAML manifests (Archipelago-specific extensions)
  • Plugin architecture - Apps are isolated Podman containers with declarative interfaces (web UI, ports, secrets)

Layers

HTTP/Transport Layer:

  • Purpose: Accept inbound connections, handle TLS termination, demultiplex HTTP/WebSocket
  • Location: core/archipelago/src/server.rs
  • Contains: Hyper listener, TCP accept loop, connection state tracking
  • Depends on: Tokio, Hyper, TLS/mTLS libraries (rustls/openssl)
  • Used by: All external clients (web UI, companion app, API consumers)

Request Routing & Auth Layer:

  • Purpose: Dispatch HTTP requests to handlers, validate sessions, enforce CSRF, rate-limit login
  • Location: core/archipelago/src/api/ (handler + rpc submodules)
  • Contains: Route matching, middleware chain, cookie extraction, error formatting
  • Depends on: Server, StateManager, SessionStore
  • Used by: All request paths; gates API access

RPC Dispatch Layer:

  • Purpose: Deserialize JSON-RPC 2.0 requests, call appropriate service method, serialize responses
  • Location: core/archipelago/src/api/rpc/mod.rs + subdirectories (auth.rs, container.rs, bitcoin.rs, etc.)
  • Contains: Method table, parameter validation, response formatting, rate limit checks
  • Depends on: All service modules
  • Used by: Frontend (WebSocket + HTTP POST to /api/v0), internal tools

Service Layer:

  • Purpose: Implement business logic — container lifecycle, identity, auth, content sync, mesh discovery
  • Location: core/archipelago/src/api/rpc/* (one RPC module per domain), plus core/archipelago/src/ (background tasks)
  • Contains: ~40 RPC method modules + 50+ core service modules (bootstrap.rs, health_monitor.rs, crash_recovery.rs, etc.)
  • Depends on: StateManager, ContainerOrchestrator, config/secrets, external services (Bitcoin, Lightning, FIPS)
  • Used by: RPC layer; other services for cross-cutting concerns (mesh, federation, webhooks)

State Management Layer:

  • Purpose: Hold canonical application state, broadcast changes to all connected clients, persist snapshots
  • Location: core/archipelago/src/state.rs (StateManager + data_model.rs)
  • Contains: RwLock, broadcast channel, revision counter
  • Depends on: DataModel (serde-serializable struct tree)
  • Used by: All services that mutate state (container ops, auth, settings)

Container Orchestration Layer:

  • Purpose: Podman lifecycle management, image verification, secret injection, crash recovery, adoption
  • Location: core/archipelago/src/container/prod_orchestrator.rs (1M+ lines; split across boot_reconciler.rs, quadlet.rs, docker_packages.rs, etc.)
  • Contains: Manifest parsing, image pull/verify, container create/start/stop, volume mounts, networking
  • Depends on: Podman CLI + socket, config parser, image registries, local filesystem
  • Used by: RPC container.* methods, BootReconciler loop, crash recovery

Frontend Layer (Vue 3):

  • Purpose: Render UI, dispatch RPC calls, maintain local UI state, handle user input
  • Location: neode-ui/src/
  • Contains: Views (pages), Components (reusable UI), Composables (logic hooks), Stores (Pinia), Router
  • Depends on: Vue 3, Vue Router, Pinia, RPC client library (custom), D3/Leaflet (charts/maps)
  • Used by: Browser clients (desktop, mobile, companion app via WebView)

Data Flow

Primary Request Path (User Action → Backend → State Sync)

  1. Frontend user interaction (click button, type input) → Vue component event handler

    • Location: neode-ui/src/views/*.vue or neode-ui/src/components/*.vue
  2. Composable dispatches RPC (e.g., useContainerInstall() calls rpc.container.install())

    • Location: neode-ui/src/composables/ (custom or imported from api/rpc-client.ts)
  3. RPC client serializes → HTTP/WebSocket POST to /api/v0

    • Location: neode-ui/src/api/rpc-client.ts
    • Payload: { jsonrpc: "2.0", method: "container.install", params: {...}, id: ... }
  4. HTTP Server receives, routes to ApiHandler

    • Location: core/archipelago/src/server.rs (listener) → core/archipelago/src/api/handler/mod.rs (dispatch)
  5. ApiHandler checks auth, extracts body, calls RpcHandler

    • Location: core/archipelago/src/api/handler/mod.rs:handle_request()
  6. RpcHandler dispatches by method name to specific RPC module

    • Location: core/archipelago/src/api/rpc/mod.rs:call() → routing to core/archipelago/src/api/rpc/container.rs:install()
  7. Service method executes (e.g., container.rs:install() calls orchestrator, updates state)

    • Location: core/archipelago/src/api/rpc/container.rs (calls methods on ContainerOrchestrator)
  8. StateManager.update_data() broadcasts the new state to all WebSocket subscribers

    • Location: core/archipelago/src/state.rs:update_data() → broadcast channel
    • All connected WebSocket clients receive { rev: N, data: {...} } update
  9. Frontend receives state update, updates Pinia stores, re-renders UI

    • Location: neode-ui/src/stores/ (Pinia stores mutate) → Vue reactivity chain → DOM update

State Management:

  • All reads from StateManager go through get_snapshot() which acquires read-lock
  • All writes go through update_data() which acquires write-lock + increments revision
  • Broadcast channel has ~100-message buffer; slow subscribers may lose old updates (by design — UI only needs latest)
  • WebSocket clients re-sync on reconnect via get_snapshot() call (full state transfer)

Secondary Flow: Scheduled Reconciliation (Convergence Loop)

  1. BootReconciler spawned at startup in main.rs

    • Location: core/archipelago/src/main.rs (line ~338-348)
  2. Reconciler runs every RECONCILER_DEFAULT_INTERVAL (~30s typical)

    • Location: core/archipelago/src/container/boot_reconciler.rs:run_forever()
  3. Compares desired manifests (disk + registry catalog) vs actual Podman state

    • Looks for: containers missing, containers orphaned, image updates, secret changes
  4. Applies remediation (create, delete, restart containers)

    • Calls: orchestrator.reconcile_*() methods
  5. Logs changes, broadcasts state update if anything changed

    • Frontend receives update, shows user the reconciled app state

This ensures apps survive crashes, OTA updates, or manual Podman edits — the desired state always converges.

Key Abstractions

ContainerOrchestrator trait:

  • Purpose: Abstract container lifecycle behind a trait so Prod (Podman-based) and Dev (in-memory) modes can coexist
  • Examples: core/archipelago/src/container/prod_orchestrator.rs, core/archipelago/src/container/dev_orchestrator.rs
  • Pattern: Trait-based strategy; RpcHandler holds Arc<dyn ContainerOrchestrator>, switches at runtime
  • Methods: create, start, stop, delete, adopt, list, reconcile, install, upgrade

Manifest (YAML-based declarative app):

  • Purpose: Fully describe an app's container, dependencies, secrets, ports, UI in one file
  • Examples: /opt/archipelago/apps/*/manifest.yml (on-disk) or registry-delivered catalogs
  • Pattern: Custom extensions over OCI/Docker Compose (e.g., interfaces.main.ui, generated_secrets)
  • Parsed into: container::manifest::Manifest struct, consumed by orchestrator

RPC Method Modules:

  • Purpose: Group related JSON-RPC methods by domain (auth, container, bitcoin, mesh, etc.)
  • Examples: core/archipelago/src/api/rpc/auth.rs, core/archipelago/src/api/rpc/bitcoin.rs
  • Pattern: Each module exports pub async fn method_name(handler, params) -> Result<Response>
  • Registration: Hardcoded dispatch in RpcHandler::call() (no reflection; methods are explicit)

BlobStore (Content-Addressed):

  • Purpose: Store attachments/files by SHA-256 hash; issue time-limited capability tokens for access
  • Examples: Used by mesh.send-content, federation attachments, backup archives
  • Pattern: Capability-based access control (CBAC); tokens scoped to issuer pubkey + hash
  • Located: core/archipelago/src/blobs.rs + core/archipelago/src/content_server.rs

StateManager + DataModel:

  • Purpose: Single source of truth for UI state; broadcast updates to all clients
  • Pattern: Read-write lock over a serde-serializable struct tree; broadcast channel for efficiency
  • Persistence: Most state is ephemeral (app listings, UI settings); durable state persists to disk separately
  • Clients: Frontend (WebSocket subscriber), internal services (read via get_snapshot), monitoring/debug

Session Store:

  • Purpose: Track authenticated HTTP sessions (cookie → user identity mapping)
  • Examples: SQLite-backed or in-memory store
  • Pattern: Session token issued at login, validated on each request, expires after TTL
  • Used by: ApiHandler auth check, rate limiter (per IP + per user)

Entry Points

Backend Daemon (Binary):

  • Location: core/archipelago/src/main.rs
  • Triggers: systemd start archipelago.service or manual ./archipelago on development node
  • Responsibilities: Parse config, init tracing, load/reconcile containers, start HTTP server, spawn background tasks
  • Key setup: Load identity → setup auth → spawn orchestrator → load manifests → start reconciler → start server

Frontend SPA:

  • Location: neode-ui/src/main.ts
  • Triggers: Browser loads /index.html (served by HTTP server from /opt/archipelago/web-ui/)
  • Responsibilities: Boot Vue app, setup Router, setup Pinia stores, establish WebSocket to backend
  • Key setup: Mount app → router ready → fetch initial state → subscribe to updates

RPC Endpoints (HTTP + WebSocket):

  • Location: core/archipelago/src/api/ (handler routes requests here)
  • Endpoint: /api/v0 (JSON-RPC 2.0 POST or WebSocket upgrade)
  • Methods: ~200+ RPCs across domains (auth, container, bitcoin, mesh, federation, etc.)
  • Example: POST /api/v0 with body {"jsonrpc": "2.0", "method": "auth.login", "params": {...}, "id": 1}

Background Tasks (Spawned at startup):

  • BootReconciler: Periodic manifest reconciliation loop
  • Health Monitor: Periodic app health checks + restart
  • Update Scheduler: Periodic app update checks
  • Mesh Service: P2P mesh listener + sender (federation, LoRa)
  • Webhook Relay: Listens for inbound webhooks, broadcasts to subscribers
  • WebSocket Listener: Upgraded HTTP connections → broadcast state subscriber
  • See: core/archipelago/src/main.rs (lines ~400-450 show the spawned tasks)

Architectural Constraints

  • Single event loop — All I/O-bound work runs on a single Tokio multi-threaded runtime; no worker threads by default (some container ops are blocking, run in tokio::task::spawn_blocking)
  • Global state via broadcast — StateManager broadcasts to all WebSocket clients; no request-response for state changes (async by design)
  • Container state mutability — Podman state can drift from manifest (manual edits, crashes); reconciler runs periodically to converge
  • No in-process data consistency — Multiple services can mutate StateManager concurrently; last write wins (fine for UI; critical ops use locks)
  • Shared blob store — All services that need to share content use the same BlobStore instance (single cap_key, single root directory)
  • Rate limiting per IP + method — Prevents brute-force login, but shared IPs see shared limits (edge case: family users, proxies)
  • Session cookie same-site — WebSocket + HTTP POST must be same-origin; CORS headers controlled by ApiHandler

Anti-Patterns

Circular RPC Dispatches

What happens: An RPC method calls back into another RPC method, forming a cycle (e.g., auth.login → container.list → auth.check_permission → auth.login) Why it's wrong: Deadlocks on RwLocks, infinite loops on state broadcasts, unclear error messages, hard to debug Do this instead: Pass check result as a side-effect from the outer method; compute permissions once at the start. Use composable patterns in frontend instead (e.g., useCanInstall() checks perms once per component mount).

Synchronous blocking in RPC handlers

What happens: RPC method calls .unwrap() on Podman command result, blocking the entire event loop Why it's wrong: One slow container op (e.g., large image pull) blocks all concurrent users Do this instead: Use tokio::task::spawn_blocking() for I/O that may take >100ms. See core/archipelago/src/container/docker_packages.rs for examples.

Hardcoding paths in app RPC modules

What happens: bitcoin.rs hardcodes /opt/archipelago/data/bitcoin.conf instead of using config.data_dir Why it's wrong: Dev mode, tests, and alternate installs all fail with "not found" Do this instead: Read from Config struct, which is passed to every RPC method. See core/archipelago/src/api/rpc/bitcoin.rs:status() for correct pattern.

Frontend state outside Pinia stores

What happens: Components use component-local ref<> for app list, duplicate the StateManager's data Why it's wrong: Stale data after OTA updates, inconsistent with other users on the same node, race conditions on install/uninstall Do this instead: Always derive from Pinia stores (e.g., useAppStore().apps). Stores subscribe to WebSocket updates. See neode-ui/src/stores/appStore.ts.

Not handling WebSocket reconnection

What happens: Frontend goes offline for 10s (network glitch), WebSocket closes, frontend doesn't re-sync state Why it's wrong: UI shows stale data (app still "installing" when actually done), user clicks again, double-action happens Do this instead: WebSocket reconnect handler should re-fetch full state (node.status, etc.), re-subscribe. See neode-ui/src/api/rpc-client.ts for the reconnect loop.

Error Handling

Strategy: Defensive layering — errors are caught at each tier, logged, and converted to user-facing messages.

Patterns:

  • HTTP layer: 4xx/5xx with JSON error (no 500s for logic errors; only for crashes)
  • RPC layer: Serialize error as { error: { code: N, message: "...", data: {...} } } per JSON-RPC spec
  • Service layer: Use anyhow::Result<T> + ? operator for early exit; convert to RpcError at handler boundary
  • Frontend: Catch RPC errors, show toast/modal, log to console (never crash the app)

Critical paths:

  • Auth failure: 401 Unauthorized + "Invalid password" (no "user not found" to leak usernames)
  • Container ops: If reconciler sees drift, logs it but continues (never crashes the daemon)
  • Image pull failure: Fallback to last-cached version if network timeout (user is never blocked on external registries)
  • Podman socket unavailable: Return 503 Service Unavailable (user sees "Archipelago is starting")

Architecture analysis: 2026-07-29