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

334 lines
21 KiB
Markdown

<!-- refreshed: 2026-07-29 -->
# Architecture
**Analysis Date:** 2026-07-29
## System Overview
```text
┌────────────────────────────────────────────────────────────────┐
│ 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<DataModel>, 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*