# 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, 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`, 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` - 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` + `?` 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*