Archipelago

A complete architecture review and learning guide for the Bitcoin Node OS — explained so anyone can understand it.

Rust + Vue 3 + Podman ~45,000 lines of Rust (213 files) ~45,500 lines of TypeScript/Vue (232 files) ~40 shell scripts v0.1.0-beta

What Is Archipelago?

Archipelago (nicknamed "Archy") is a personal server operating system focused on Bitcoin. You download an ISO file, flash it to a USB drive, install it on any computer, and it gives you:

Think of it like an iPhone for servers. Apple gives you a phone with an App Store where you install apps. Archipelago gives you a server with a Marketplace where you install self-hosted apps. The difference? You own and control everything — your data never leaves your machine.

Similar projects exist (Umbrel, Start9, RaspiBlitz), but Archipelago is built from scratch with production-grade security and a custom Rust backend instead of Node.js.

The Big Picture

Before diving into code, understand the four layers of the system and how they stack:

┌──────────────────────────────────────────────────────┐ │ YOUR BROWSER │ │ (Vue.js Single Page Application) │ └──────────────────────┬───────────────────────────────┘ │ HTTP requests (fetch API) ┌──────────────────────┴───────────────────────────────┐ │ NGINX │ │ Reverse proxy — routes traffic to the right place │ │ /rpc/v1 → backend /app/bitcoin/ → container │ └──────────────────────┬───────────────────────────────┘ │ Internal HTTP (port 5678) ┌──────────────────────┴───────────────────────────────┐ │ RUST BACKEND │ │ The brain — handles auth, app installs, Bitcoin │ │ RPC, mesh networking, federation, health checks │ └──────────────────────┬───────────────────────────────┘ │ Podman REST API (Unix socket) ┌──────────────────────┴───────────────────────────────┐ │ PODMAN CONTAINERS │ │ Bitcoin Core, LND, Mempool, Nextcloud, etc. │ │ Each app runs isolated in its own container │ └──────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────┐ │ DEBIAN 12 (Linux OS) │ │ The foundation — systemd, firewall, filesystem │ └──────────────────────────────────────────────────────┘
Key Concept: Separation of Concerns Each layer has ONE job. The browser shows things. Nginx routes traffic. Rust makes decisions. Podman runs apps. This makes the system easier to understand, test, and fix — if the UI breaks, you know the problem is in the Vue code, not the Rust code.

How It Runs on a Machine

When you install Archipelago on a computer and power it on, here's what happens in order:

1

Linux boots — Debian 12 starts up, loads drivers, mounts disks

2

systemd starts services — A program called systemd reads archipelago.service and launches the Rust backend

3

Rust backend initializes — Loads config, creates/loads encryption keys, starts the HTTP server on port 5678

4

Health monitor starts — Checks which containers are running, restarts crashed ones, reports readiness

5

Nginx starts — Listens on port 80 (HTTP) and routes all incoming traffic

6

Containers start — Bitcoin, LND, and other apps start in priority order (Bitcoin first, then things that depend on it)

7

Ready! — You open a browser, go to your server's IP address, and see the dashboard

It's like starting a restaurant. First the building opens (Linux). Then the manager arrives (Rust backend). They check if all kitchen stations are ready (health monitor). The front door opens (Nginx). The cooks start preparing (containers). Customers can now order (you open the web UI).

The Four Layers — Detailed

Layer 1: The Rust Backend (The Brain)

This is the most important piece. It's written in Rust — a programming language known for speed and safety. The backend is the "brain" that controls everything.

Why Rust? Rust prevents entire categories of bugs (memory leaks, crashes, race conditions) at compile time. For a server that manages Bitcoin wallets and runs 24/7, this matters. A crash could mean lost money. Rust makes crashes nearly impossible.

How the code is organized

The Rust code lives in core/ and is split into 5 workspace crates (consolidated from 9 during recent refactoring):

CrateWhat It DoesLinesAnalogy
archipelago The main binary. API endpoints, auth, identity, federation, mesh networking, monitoring, health checks ~42,000 The restaurant manager — coordinates everything
container PodmanClient (REST API socket), manifest parser, dependency resolver, health monitor, Bitcoin simulator ~2,060 The kitchen manager — controls cook stations
security Encrypted secrets (Argon2 + ChaCha20-Poly1305), AppArmor profiles, Cosign image verification ~743 The security guard — locks doors, checks IDs
parmanode Compatibility layer for migrating from an older project ~234 A translation book — speaks the old language
performance CPU, memory, and disk resource management ~92 The meter reader — watches resource gauges

Key modules you should know

The recent refactoring split monolithic files into focused module directories. Each directory has a mod.rs entry point and focused sub-files:

ModuleWhat It DoesLinesStructure
main.rsEntry point — starts server, registers signal handlers~180Single file
server.rsWires HTTP server, connects all components~506Single file
api/handler/HTTP request routing, CORS, WebSocket upgrade, auth~896mod.rs + content, dwn, node_message, proxy, websocket
api/rpc/RPC dispatch, 29 endpoint modules + 8 subdirectories~20,000dispatcher.rs routes to focused handlers
api/rpc/package/App lifecycle — install, config, runtime, progress, deps~2,248config.rs, install.rs, lifecycle.rs, runtime.rs, stacks.rs, dependencies.rs, progress.rs
mesh/LoRa mesh networking — protocol, crypto, serial, relay~6,00013 files + listener/ subdirectory (6 files)
federation/Multi-node federation — invites, sync, storage~782invites.rs, storage.rs, sync.rs, types.rs
credentials/W3C Verifiable Credentials — CRUD, presentation~803operations.rs, presentation.rs, store.rs, types.rs
monitoring/Metrics collection, alerts, beta telemetry~1,380collector.rs, store.rs, alerts.rs, telemetry.rs, notifications.rs, types.rs
session.rsSession management, remember-me, cookie handling~622Single file
health_monitor.rsContainer health, auto-restart, system alerts~731Single file
rate_limit.rsPer-IP login + endpoint rate limiting~191Single file (new)

How the backend handles a request

Browser sends: POST /rpc/v1 Body: { "method": "package.install", "params": { "id": "bitcoin-knots" } } Step 1: Nginx receives it on port 80, forwards to port 5678 Step 2: Rust HTTP server (Hyper) receives the raw bytes Step 3: handler/mod.rs parses the JSON, extracts the method name Step 4: rpc/mod.rs checks the CSRF token (security check) Step 5: rpc/mod.rs checks the session cookie (are you logged in?) Step 6: dispatcher.rs routes to package/install.rs based on method name Step 7: package/install.rs validates the app ID Step 8: package/dependencies.rs checks dependency chain Step 9: PodmanClient pulls image + creates container via REST API socket Step 10: Response sent back: { "result": { "state": "installing" } }

Rust Backend Deep Dive — Should We Use Custom Code?

The short answer: Yes, custom Rust is the right call for Archipelago. The backend does things no off-the-shelf tool provides: it orchestrates rootless Podman containers, manages Bitcoin/LND RPC, handles encrypted secrets, runs federation/mesh networking, and serves a real-time WebSocket to the Vue frontend — all as a single binary with zero runtime dependencies. The alternatives (Node.js, Go, Python) would need dozens of third-party packages to match, and none offer Rust's memory safety guarantees for a server handling Bitcoin keys.

Why not use an existing solution?

Projects like Umbrel use a Node.js + Docker Compose backend. Start9 uses Rust (like us). RaspiBlitz uses bash scripts. Here's why custom Rust wins:

ApproachProsCons
Node.js (Umbrel-style) Fast to develop, large ecosystem Memory-unsafe (crypto bugs), GC pauses, runtime dependency, node_modules supply chain risk
Bash scripts (RaspiBlitz-style) Simple, no compilation Unmaintainable at scale, no type safety, fragile error handling, injection risks
Go Single binary, good concurrency No zero-cost abstractions, GC pauses, weaker type system than Rust
Rust (our choice) Single binary, zero-cost abstractions, memory safety without GC, excellent crypto ecosystem, zeroize for key material Steeper learning curve, slower compile times

RPC Endpoint Architecture (Refactored)

Every action the frontend takes goes through POST /rpc/v1 as a JSON-RPC call. The RPC layer was recently refactored from monolithic files into 29 standalone modules + 8 domain subdirectories, totaling ~20,000 lines. Requests flow through dispatcher.rs (395 LOC) which routes to the appropriate handler:

CategoryModuleLinesKey Methods
App Lifecycle package/ (7 files)2,248 package.install, package.start, package.stop, package.uninstall, package.stacks
container.rs413 container.logs, container.inspect
marketplace.rs225 marketplace.list, marketplace.search
Auth & Security auth.rs102 auth.login, auth.login.totp, auth.logout
totp.rs295 totp.enable, totp.verify, totp.disable
credentials.rs274 credentials.issue, credentials.verify (W3C Verifiable Credentials)
security.rs66 AppArmor policy management
Bitcoin bitcoin.rs96 bitcoin.getblockchaininfo, bitcoin.getpeerinfo — RPC passthrough
lnd/ (5 files)1,092 lnd.getinfo, lnd.walletbalance, lnd.channels, lnd.payments
wallet.rs108 wallet.balance, wallet.transactions
System system/ (2 files)777 system.stats, system.reboot, system.factory-reset
monitoring.rs216 monitoring.containers, monitoring.resources
update.rs108 update.check, update.apply
Identity & Federation identity/ (2 files)778 identity.create, identity.export, identity.did
federation/ (2 files)732 federation.list-nodes, federation.pair, federation.sync
mesh/ (6 files)885 mesh.status, mesh.send, mesh.peers, mesh.bitcoin-ops
Network tor/ (2 files)769 tor.status, tor.create-service, tor.get-address
vpn.rs229 vpn.status, vpn.configure
Other analytics.rs438 Event analytics, usage tracking
interfaces.rs442 Network interface management
backup_rpc.rs394 backup.create, backup.restore
content.rs352 Peer content distribution
transport.rs157 Transport layer abstraction
Refactoring win: The old monolithic package.rs (1,795 lines) was split into 7 focused files under package/. Similarly, federation.rs, identity.rs, mesh.rs, system.rs, and tor.rs were each extracted into their own subdirectories with handlers.rs + mod.rs separation. The API handler layer (handler.rs) was split into 6 focused files under api/handler/.

Container Orchestration — How Podman Is Controlled

The backend talks to rootless Podman via its Unix socket REST API (not CLI). This is faster, more reliable, and avoids shell injection risks.

PodmanClient connects to: /run/user/1000/podman/podman.sock (API v4.0.0) Install flow: 1. package.rs validates app ID + checks dependencies 2. DependencyResolver topological sort → install order 3. PodmanClient.pull_image() → downloads container image 4. PodmanClient.create_container() → sets ports, volumes, caps, memory limits 5. PodmanClient.start_container() 6. HealthMonitor begins watching (60s intervals) Crash recovery: On startup → check PID marker → if unclean shutdown: → Restart containers in tier order: Tier 0: Databases (postgres, redis, mariadb) Tier 1: Core infra (bitcoin-knots) Tier 2: Dependent services (lnd, electrs, nbxplorer) Tier 3: Applications (mempool, btcpay, fedimint) Tier 4: Frontends (mempool-web, lnd-ui) → Respect user-stopped.json (don't restart manually stopped apps) → Max 3 restart attempts with exponential backoff (10s → 30s → 90s)

Security Architecture

LayerMechanismImplementation
Secrets at rest AES-256-GCM encryption core/security/secrets_manager.rs — encrypts to /var/lib/archipelago/secrets/
Node identity Ed25519 keypair Generated on first boot, stored at /var/lib/archipelago/identity/
Image verification Cosign signatures core/security/image_verifier.rs — verifies container image provenance
Sessions 32-byte random tokens OsRng, 24h TTL, persisted to sessions.json, zeroized on drop
2FA TOTP (RFC 6238) 5 attempt lockout, 5min pending session TTL, token rotation after verification
Rate limiting Per-IP + per-endpoint Login endpoints rate-limited, IP extracted from X-Real-IP (loopback only)
RBAC Explicit method allowlists No prefix matching — each role lists exact permitted methods
Key material zeroize::Zeroizing All crypto keys zeroed from memory after use

WebSocket Real-Time Sync

The frontend connects to /ws/db and receives the full DataModel on connect, then incremental updates as state changes. This is how the UI shows live container status, sync progress, and notifications without polling.

DataModel (broadcast to all WebSocket clients): { server_info: { node_id, name, tor_address, lan_ip, version } package_data: { "bitcoin-knots": { state: "running", health: "healthy", ... } "lnd": { state: "running", health: "healthy", ... } "mempool": { state: "stopped", health: null, ... } } peer_health: { "did:key:z6Mk...": true } notifications: [ { type: "warning", message: "Disk 85% full" } ] }

What's custom vs. what could be replaced?

ComponentCustom?Could it be replaced?
HTTP server Uses hyper (standard Rust HTTP) Could use axum or actix-web for ergonomics, but hyper is fine
RPC routing Custom — hand-rolled JSON-RPC dispatcher Could use jsonrpsee or generate from OpenAPI, but the current router is simple and works
Container orchestration Custom — PodmanClient + health monitor No off-the-shelf alternative for rootless Podman orchestration with Bitcoin-specific dependency ordering
Secrets management Custom — AES-256-GCM with Zeroize Could use age or sops, but inline encryption is simpler for container secrets
Federation/Mesh Custom — Ed25519 signed messages, Nostr discovery, DWN No existing solution does Bitcoin node federation + mesh radio. This is novel.
Auth/Sessions Custom Could use a library, but the session model is simple (32-byte tokens + file persistence)
Bitcoin/LND RPC Custom passthrough Must be custom — proxies authenticated calls to local Bitcoin/LND with macaroon management
Bottom line: The custom code isn't reinventing the wheel — it's glue that connects Podman, Bitcoin, LND, Tor, Nostr, mesh radios, and a Vue frontend into a cohesive OS. No existing framework does this. The individual pieces (hyper, serde, tokio, ed25519-dalek, aes-gcm) are all battle-tested crates. The custom part is the orchestration logic that ties them together.

Layer 2: The Vue.js Frontend (The Face)

The frontend is what you see in the browser. It's built with Vue 3 — a JavaScript framework for building interactive web pages — and TypeScript — JavaScript with type safety.

What is a Single Page Application (SPA)? Instead of loading a new HTML page every time you click something (like old websites), an SPA loads once and then dynamically updates the page content. When you click "Marketplace" in Archipelago, it doesn't load a new page — it swaps out the content area. This makes it feel fast and smooth, like a native app.

Frontend file structure (refactored)

The frontend was heavily refactored — large "god components" were split into focused sub-views, and the god store was decomposed into dedicated stores:

neode-ui/src/
├── api/              ← Backend communication (4 files + 1 service)
│   ├── rpc-client.ts    ← RPC client (18.8 KB) — ~70 methods, retry, CSRF
│   ├── websocket.ts     ← WebSocket (16.3 KB) — JSON patch (RFC 6902)
│   ├── container-client.ts ← Container API helpers
│   ├── filebrowser-client.ts ← FileBrowser API
│   └── services/contextBroker.ts ← Context management (21.9 KB)
├── views/            ← 37 top-level + 47 sub-views in 14 subdirectories
│   ├── Dashboard.vue    ← Main layout with sidebar
│   ├── dashboard/     ← Sidebar, MobileNav, ConnectionBanner (6 files)
│   ├── apps/          ← AppCard, UninstallModal, config (5 files)
│   ├── appDetails/    ← HeroSection, ContentSection, Sidebar (4 files)
│   ├── appSession/    ← Frame, Header, NostrBridge, AppIdentity (5 files)
│   ├── discover/      ← Hero, AppGrid, FeaturedApps, FilterModal (6 files)
│   ├── federation/    ← Header, NodeList, JoinModal, RotateDid (8 files)
│   ├── fleet/         ← NodeGrid, ContainerMatrix, Alerts, Overview (6 files)
│   ├── mesh/          ← BitcoinPanel, DeadmanPanel, styles (3 files)
│   ├── settings/      ← 13 focused sections (Account, 2FA, Backup, etc.)
│   ├── web5/          ← 14 sub-views (DID, Wallet, Nostr, DWN, etc.)
│   ├── marketplace/   ← AppCard, FilterModal, marketplaceData (3 files)
│   ├── server/        ← QuickActions, Modals, TorServices (3 files)
│   └── home/          ← SystemCard, WalletCard (2 files)
├── components/       ← 31 reusable components + 6 in subdirectories
│   ├── BootScreen.vue, SplashScreen.vue, SpotlightSearch.vue
│   ├── BaseModal.vue, ToastStack.vue, SkeletonCard.vue, EmptyState.vue
│   ├── MeshMap.vue, LineChart.vue, AnimatedLogo.vue
│   ├── cloud/         ← FileCard, FileGrid, ShareModal (5 files)
│   └── federation/    ← NetworkMap.vue
├── stores/           ← 18 Pinia stores (decomposed from god store)
│   ├── app.ts           ← Core app state (slimmed down)
│   ├── auth.ts          ← Login, logout, TOTP, sessions (NEW)
│   ├── server.ts        ← Server state, package actions (NEW)
│   ├── sync.ts          ← WebSocket, real-time data, JSON patch (NEW)
│   ├── container.ts     ← Container states & lifecycle
│   ├── mesh.ts          ← Mesh networking (14 KB — largest store)
│   ├── appLauncher.ts   ← App iframe management (11 KB)
│   └── ... 11 more focused stores
├── composables/      ← 11 composables + 10 test files
│   ├── useToast.ts, useControllerNav.ts (16.9 KB)
│   ├── useLoginSounds.ts, useNavSounds.ts, useAudioPlayer.ts
│   └── useOnboarding.ts, useModalKeyboard.ts, useMobileBackButton.ts
├── types/            ← TypeScript type definitions (3 files)
│   ├── api.ts           ← RPC methods, responses, DataModel, PatchOperation
│   └── aiui-protocol.ts ← AIUI communication protocol
├── router/           ← Route definitions (9.5 KB) — lazy-loaded + nav guards
└── style.css            ← Global glassmorphism theme + Tailwind utilities

How a Vue component works

Every .vue file has three sections:

<!-- 1. THE LOGIC (TypeScript) -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { rpcClient } from '@/api/rpc-client'

// "ref" is a reactive variable — when it changes, the UI updates automatically
const apps = ref([])
const loading = ref(true)

// "onMounted" runs when the component first appears on screen
onMounted(async () => {
  apps.value = await rpcClient.getMarketplace()
  loading.value = false
})
</script>

<!-- 2. THE TEMPLATE (HTML with Vue directives) -->
<template>
  <div v-if="loading">Loading...</div>
  <div v-else v-for="app in apps" class="glass-card">
    {{ app.name }}
  </div>
</template>

<!-- 3. THE STYLES (CSS, scoped to this component) -->
<style scoped>
  /* Styles here only affect THIS component */
</style>

A Vue component is like a LEGO brick. Each brick (component) has its own shape (template), color (styles), and moving parts (script). You snap them together to build the full UI. The <Dashboard> component contains <Sidebar>, which contains <NavItem> components — just like nesting LEGO bricks.


Layer 3: The Container System (The Apps)

Containers are how Archipelago runs apps like Bitcoin Core, Lightning, Nextcloud, etc. Each app runs in its own isolated "box" called a container.

What is a Container? A container is like a lightweight virtual machine. It has its own filesystem, its own network, and its own processes — but it shares the host's Linux kernel, so it's much faster than a full VM. Think of it as an apartment in a building — each apartment has its own walls and locks, but they all share the same building infrastructure.

Archipelago uses rootless Podman instead of Docker. Podman runs entirely without root privileges under the archipelago user (UID 1000) — no background daemon, no root access needed. The backend communicates with Podman via its REST API socket, not the CLI.

Container security rules

Every container in Archipelago follows strict security rules:

RuleWhat It MeansWhy
--cap-drop=ALL Remove all Linux capabilities (super-powers) A hacked container can't do anything dangerous
--cap-add=CHOWN Give back only the specific powers needed Minimum privilege — only what's necessary
readonly_root: true Container can't modify its own program files Prevents malware from modifying the app
--user 1001:1001 Run as non-root user Even if exploited, can't access system files
no-new-privileges Can't escalate to higher permissions Prevents privilege escalation attacks

Container startup order (tiers)

Tier 1: Foundation (start first, other apps depend on these) ├── Bitcoin Core/Knots ← The blockchain ├── MySQL/PostgreSQL ← Databases └── Redis ← Cache Tier 2: Core Services (need Tier 1 to be running) ├── LND (Lightning) ← Needs Bitcoin ├── ElectrumX ← Needs Bitcoin ├── Mempool ← Needs Bitcoin + ElectrumX └── BTCPay Server ← Needs Bitcoin + LND Tier 3: Applications (independent or need Tier 2) ├── Nextcloud, Jellyfin ← File storage, media ├── Vaultwarden ← Password manager ├── Home Assistant ← Smart home └── Grafana ← Monitoring dashboards

Layer 4: Nginx (The Traffic Cop)

Nginx (pronounced "engine-X") is a web server that sits between the internet and everything else. Every single request goes through it first. Archipelago's nginx config is ~1,100 lines — one of the most complex parts of the system.

Nginx is like the receptionist at a hospital. You walk in and say what you need. "I need the API" — they send you to the Rust backend. "I need the Bitcoin app" — they send you to the Bitcoin container. "I need the website" — they hand you the static files. Without the receptionist, you'd be wandering the hallways lost.

Why Nginx? Comparing Reverse Proxies

Every node OS needs a reverse proxy to route traffic. Here's how the major projects differ:

Nginx Archipelago

Battle-tested (30+ years)
Sub-millisecond routing
Fine-grained rate limiting
sub_filter HTML rewriting
Full CSP / HSTS control
~ Manual config (1,100 lines)
No auto-TLS (manual certs)

Caddy Umbrel

Automatic HTTPS / Let's Encrypt
Simple Caddyfile syntax
Built-in HTTP/3 support
No sub_filter (needs plugins)
Higher memory footprint
Less granular rate limiting
~ Newer, smaller ecosystem

Tor-only StartOS

Maximum privacy (no clearnet)
No port forwarding needed
Built-in NAT traversal
Slow (500ms–3s latency)
No LAN access without config
Requires .onion browser support
No WebSocket over Tor (flaky)

NixOS Module Nix-Bitcoin

Declarative, reproducible
Atomic rollbacks
Any proxy (Nginx/Caddy/HAProxy)
~ Steep learning curve (Nix lang)
No web UI (CLI only)
Not beginner-friendly
Long rebuild times

Archipelago's choice: Nginx gives the most control over security headers, rate limiting, and HTML rewriting (injecting Nostr provider scripts into app iframes). The tradeoff is a 1,100-line config instead of a 50-line Caddyfile — but for a Bitcoin node OS, that control is worth it.

Head-to-Head: Architecture Decisions

Feature Archipelago Umbrel StartOS Nix-Bitcoin RaspiBlitz
Reverse Proxy Nginx Caddy Tor hidden svc Nginx (Nix module) Nginx
Backend Rust Node.js + Go Rust (startos) Shell/Nix Shell scripts
Containers Rootless Podman Docker (root) Docker (root) None (native pkgs) Docker (root)
TLS/HTTPS Self-signed + HSTS Auto (Let's Encrypt) Tor-only (no TLS) Let's Encrypt Self-signed
Rate Limiting Dual-zone (RPC 20r/s + Auth 3r/s) None None Optional (manual) None
Security Headers Full CSP + HSTS + Permissions Basic N/A (Tor) Configurable Minimal
App Isolation Cap-drop, readonly root, non-root UID Docker defaults Docker + sandboxing systemd sandboxing Docker defaults
LAN + Remote LAN + Tailscale + Tor LAN + Tor + Tailscale Tor-only (LAN optional) LAN + WireGuard LAN + Tor
WebSocket Native (24h timeout) Polling + WS SSE over Tor N/A Polling
App UI Injection sub_filter (Nostr NIP-07) None None N/A None

How Nginx Routes Traffic

The config defines 30+ location blocks across HTTP (port 80) and HTTPS (port 443). Here are the major routing categories:

Backend & API Routes

URL PatternBackendRate LimitTimeoutPurpose
/rpc/:567820r/s (burst 40)600sAll RPC API calls (1MB body limit)
/ws:567886,400s (24h)WebSocket — real-time state updates
/health:5678defaultHealth check (no auth)
/archipelago/:5678defaultSystem endpoints
/content:5678defaultPeer content sharing
/dwn:5678defaultDecentralized Web Node
/electrs-status:5678defaultElectrum sync status (CORS enabled)
/lnd-connect-info:5678defaultLND connection URI (CORS enabled)

App Proxies — 24 Container Apps

Every /app/{id}/ route proxies into a container. All share a common pattern: strip the upstream X-Frame-Options, set SAMEORIGIN, inject the Nostr provider script, and forward real IP headers.

AppPortSpecial Config
bitcoin-ui8334
mempool4080300s timeouts
lnd8081300s timeouts
electrumx50002
btcpay23000
fedimint8175300s timeouts
fedimint-gateway8176300s timeouts
filebrowser808310GB uploads, path traversal blocking
nextcloud8085300s timeouts
vaultwarden8082
immich2283300s timeouts
jellyfin8096
grafana3000
portainer9000
uptime-kuma3001
searxng8888
ollama11434
indeedhub7777URL rewriting, WS, 30-day asset cache
homeassistant812386,400s timeout (persistent)
penpot9001300s timeouts
photoprism2342
onlyoffice8044
endurain8080
nginx-proxy-manager8181

AIUI Routes (AI Chat Interface)

The AI chat UI has its own set of proxied API backends — all require a valid session cookie or return 401.

URL PatternBackendTimeoutPurpose
/aiui/Static filesChat UI (no-cache for HTML)
/aiui/api/claude/:3142300s readClaude proxy (streaming, no buffering)
/aiui/api/ollama/:11434300s readLocal Ollama model (streaming)
/aiui/api/openrouter/openrouter.ai120sExternal AI API (SSL passthrough)
/aiui/api/web-search:888830sSearXNG search (503 JSON on failure)

Security Headers — How Archipelago Compares

Security headers tell the browser what's allowed and what isn't. Here's what each node OS sends:

Header Archipelago Umbrel StartOS RaspiBlitz
Content-Security-Policy Full self + WS + frame-src Basic None None
HSTS 1 year + includeSubDomains Yes N/A (Tor) No
X-Frame-Options SAMEORIGIN Varies None None
X-Content-Type-Options nosniff nosniff None None
Permissions-Policy All blocked None None None
Referrer-Policy strict-origin None None None
Rate Limiting Dual-zone None None None
Archipelago leads on security headers. Most node OS projects ship with minimal or no HTTP security headers. Archipelago sets a full Content-Security-Policy, HSTS with 1-year max-age, Permissions-Policy blocking camera/microphone/geolocation/payment, and dual-zone rate limiting — defense-in-depth at the proxy layer.

Unique Nginx Features in Archipelago

Nostr NIP-07 Injection

  • Every app proxy uses sub_filter to inject nostr-provider.js into </head>
  • Gives all container apps window.nostr for signing
  • No other node OS does this — unique to Archipelago
  • Accept-Encoding disabled to enable text rewriting

Dual Rate Limit Zones

  • rpc zone: 20 req/s base, burst of 40 — for API calls
  • auth zone: 3 req/s — for login/auth endpoints (brute-force protection)
  • Returns HTTP 429 on violation
  • Per-IP tracking with 10MB shared memory zone

External Site Proxying

  • /ext/botfights/, /ext/484-kitchen/, etc. proxy external HTTPS sites
  • Strips CORS/COEP/COOP headers for iframe embedding
  • Rewrites href/src attributes to rebase paths
  • Standalone proxy servers on ports 8901–8903

FileBrowser Security

  • Path traversal blocked: /\.\. patterns return 403
  • 10GB upload limit (client_max_body_size 10G)
  • proxy_request_buffering off for streaming large uploads
  • Separate protection for /api/resources/ and /api/raw/ paths

SSL/TLS Configuration

  • TLSv1.2 + TLSv1.3 only (no older protocols)
  • Modern cipher suite: ECDHE-ECDSA + ECDHE-RSA with AES-GCM
  • Self-signed certificate at /etc/archipelago/ssl/
  • Dual-server setup: port 80 (HTTP) + port 443 (HTTPS)

IndeedhHub Complexity

  • Most complex app proxy: URL rewriting, WebSocket, caching
  • _next/ assets cached 30 days with immutable
  • WebSocket at /app/indeedhub/ws/ with 24h timeout
  • Rewrites both single and double quoted href/src

Nginx Config File Map

FileLinesPurpose
image-recipe/configs/nginx-archipelago.conf~1,100Production config — HTTP + HTTPS servers, all routing
image-recipe/configs/snippets/archipelago-https-app-proxies.conf~400HTTPS app proxy blocks (included in main config)
image-recipe/configs/snippets/archipelago-pwa.conf~30PWA service worker and manifest caching
image-recipe/configs/external-app-proxies.conf~200External site reverse proxies (BotFights, 484 Kitchen)
neode-ui/docker/nginx.conf~60Dev Docker config (mock backend on :5959)
neode-ui/docker/nginx-demo.conf~80Demo mode config (no security, mock backend)
docker/bitcoin-ui/nginx.conf~50Bitcoin UI container — RPC proxy with CORS
docker/electrs-ui/nginx.conf~30Electrs UI container — status endpoint
docker/lnd-ui/nginx.conf~30LND UI container — connect info
indeedhub/nginx.conf~200IndeedhHub container — MinIO, API, relay, SPA
Why so many nginx configs? There are three layers of nginx: (1) the main server nginx that routes all traffic, (2) per-app container nginx configs inside some containers (bitcoin-ui, electrs-ui, lnd-ui, indeedhub) that serve their own SPAs and proxy to internal services, and (3) dev/demo nginx configs for local development. Changes to app routing require updating BOTH the main config AND the relevant container config.

How Data Flows Through the System

Let's trace what happens when you click "Install Bitcoin" in the UI:

1

You click the Install button in Marketplace.vue. Vue calls the Pinia store action installPackage('bitcoin-knots')

2

The store calls the RPC client: rpcClient.installPackage('bitcoin-knots', 'docker.io/bitcoin/knots:28')

3

RPC client sends HTTP POST to /rpc/v1 with a session cookie and CSRF token for security

4

Nginx receives the request on port 80, checks rate limits, forwards to the Rust backend on port 5678

5

Rust backend validates — checks your session is valid, CSRF token matches, app ID is safe (no shell injection characters)

6

Rust checks dependencies — if you're installing LND, it checks Bitcoin is already running

7

Rust tells Podman to pull the imagepodman pull docker.io/bitcoin/knots:28 (downloads the app)

8

Rust creates and starts the container with all security flags (cap-drop, readonly root, etc.)

9

Backend sends a WebSocket update — the frontend receives a "state changed" event in real time

10

Vue reactively updates the UI — the Marketplace card changes from "Install" to "Running" with no page reload

RPC: How Frontend Talks to Backend

RPC stands for Remote Procedure Call. It's a way for the frontend to tell the backend "do something" — like calling a function on a remote computer.

RPC vs REST Most web APIs use REST (different URLs for different things: GET /users, POST /users, DELETE /users/5). Archipelago uses RPC instead — every request goes to the same URL (/rpc/v1) and the method name says what to do. It's like having one phone number for a building, and you say who you want to talk to.

The frontend has a class called RPCClient (in rpc-client.ts) with ~70 methods. Each method maps to a backend function:

Frontend MethodBackend HandlerWhat It Does
rpcClient.login(password)auth.loginLog in with password
rpcClient.getServerInfo()system.infoGet server name, version, uptime
rpcClient.installPackage(id, image)package.installInstall a container app
rpcClient.getBitcoinInfo()bitcoin.infoGet blockchain sync %, block height
rpcClient.sendMeshMessage(text)mesh.sendSend a message over LoRa radio

Built-in resilience

The RPC client has built-in protections:

State Management

State is the data your app is currently working with: is the user logged in? What apps are installed? Is Bitcoin synced? This data needs to be shared between components.

What is Pinia? Pinia is Vue's state management library. Instead of each component keeping its own data (which leads to chaos), you put shared data in a "store" — a central place that any component can read from and write to. When the store changes, every component that uses it updates automatically.

Archipelago has 18 Pinia stores (up from 15 — the "god store" was decomposed):

app.ts slimmed

Core app state — slimmed down after extracting auth, server, and sync concerns

auth.ts new

Authentication state machine — login, logout, TOTP, session management

server.ts new

Server computed state + RPC action proxies (install, restart, update)

sync.ts new

WebSocket connection + real-time JSON patch (RFC 6902) data sync

container.ts good

Container lifecycle — running, stopped, installing states (9.2 KB)

mesh.ts good

LoRa radio state — device, peers, messages, channels (14 KB)

appLauncher.ts good

App iframe management, Nostr consent, port mapping (11 KB)

aiPermissions.ts good

AI data access permission management (5.2 KB)

Store decomposition complete. The old "god store" (app.ts) that handled auth + WebSocket + server data + package management was split into three new focused stores: auth.ts (authentication state machine), server.ts (server state + RPC actions), and sync.ts (WebSocket + data synchronization). The login flow is now: useAuthStore().login()useSyncStore().initializeData() + connectWebSocket() → views consume sync.data reactively.

WebSocket: real-time updates

Instead of the frontend asking "has anything changed?" every second (polling), the backend pushes updates to the frontend through a WebSocket — a persistent, two-way connection.

Traditional polling (slow, wasteful): Frontend: "Anything new?" → Backend: "No" (every 1 second) Frontend: "Anything new?" → Backend: "No" Frontend: "Anything new?" → Backend: "Yes! Bitcoin synced!" WebSocket (fast, efficient): Frontend ←→ Backend: persistent connection Backend: "Bitcoin synced!" → Frontend instantly updates Backend: "New container started!" → Frontend instantly updates

Authentication & Sessions

When you log in, the backend creates a session — a temporary "you're allowed in" token. Here's how it works:

1

You enter your password on the login page

2

Backend hashes it with bcrypt — a one-way function that makes it impossible to reverse

3

Backend compares the hash to the stored hash (never compares raw passwords)

4

Backend creates a session — generates a random 256-bit token using a cryptographically secure random number generator

5

Session ID sent as a cookie — the browser stores it and sends it with every request

6

CSRF token also sent — a second token that prevents cross-site request forgery attacks

Why two tokens? The session cookie proves you're logged in. The CSRF token proves the request came from YOUR browser tab, not a malicious website that tricked your browser into sending a request. Both must match for any request to succeed.

Security Model

Archipelago is a defense-in-depth system — multiple layers of security so that if one fails, others still protect you.

LayerProtectionAgainst What
OS UFW firewall, AppArmor profiles Network attacks, process escape
Nginx Rate limiting, security headers, HSTS DDoS, XSS, clickjacking
Backend CSRF validation, session auth, input sanitization CSRF, injection, unauthorized access
Containers Capability dropping, readonly root, non-root user Container escape, privilege escalation
Crypto ChaCha20-Poly1305 encryption, Argon2 key derivation, ed25519 signatures Data theft, key compromise, impersonation
Network Tor routing, onion services Traffic analysis, IP exposure

Bitcoin Integration

Bitcoin is the heart of Archipelago. The backend communicates with Bitcoin Core/Knots using JSON-RPC — the same protocol Bitcoin has used since 2009.

Critical Rule: Never Use Floating Point for Bitcoin Bitcoin amounts are always in satoshis (1 BTC = 100,000,000 sats) as integers. Using floating point (decimals) causes rounding errors. 0.1 + 0.2 ≠ 0.3 in floating point. When you're dealing with money, that's unacceptable. Archipelago uses u64 in Rust and BigInt in TypeScript for all Bitcoin amounts.

Bitcoin RPC examples

// The backend calls Bitcoin Core like this:
bitcoin_rpc("getblockchaininfo")   → sync progress, block height
bitcoin_rpc("getnetworkinfo")      → peer count, version
bitcoin_rpc("getmempoolinfo")      → unconfirmed transaction count
bitcoin_rpc("estimatesmartfee", 6) → fee estimate for 6-block confirmation

Federation & Multi-Node

Multiple Archipelago nodes can form a federation — a trusted network of servers that sync data, share state, and communicate privately.

Your Node (.228) ←── Tor ──→ Friend's Node │ │ └──── Tor ──→ Office Node ←── Tor ──┘ Each node has: • Ed25519 identity key (cryptographic identity) • DID (Decentralized Identifier — like a username that can't be taken away) • Onion address (Tor hidden service — no IP address exposed) • DWN (Decentralized Web Node — stores and syncs data)

Nodes discover each other through Nostr relays (publish presence, but never onion addresses — those are exchanged privately via encrypted DMs).

Mesh Networking

Archipelago can communicate over LoRa radio — no internet needed. A small radio device plugs into the server's USB port and sends messages up to 10+ km using the Meshtastic/Meshcore protocol.

Imagine walkie-talkies that can send text messages. Each radio can relay messages for others, so even if two radios can't reach each other directly, they can communicate through intermediate radios. That's mesh networking — no cell towers, no ISPs, no internet required.

Deploy System

The deploy script (scripts/deploy-to-target.sh) is how code gets from your development laptop to the live server. It's a ~1,790-line shell script (with shared functions from lib/common.sh) that automates everything:

1

Pre-flight checks — verifies SSH connectivity, checks git state, warns about uncommitted changes

2

Frontend build — runs npm run build to compile Vue/TypeScript into static files

3

Upload frontend — rsyncs built files to /opt/archipelago/web-ui/ on the server

4

Upload Rust source — rsyncs core/ to the server (builds ON the server, not macOS)

5

Build on server — runs cargo build --release on the Linux server

6

Sync configs — copies nginx config, systemd service from image-recipe/configs/

7

Restart services — reloads nginx, restarts the Rust backend via systemd

8

Health check — pings /health endpoint to verify everything came back up

9

Deploy manifest — writes a JSON file recording the commit, timestamp, and deploy status

Why build on the server? Rust compiles to machine code specific to the CPU architecture. If you compile on macOS (ARM/x86) and copy the binary to a Linux server, it won't run — you get an "Exec format error". The deploy script sends the source code and compiles on the target machine.

ISO Build Process

The ISO build creates the installer that users flash to USB. It's a ~1,870-line script that:

  1. Downloads a Debian 12 Live ISO as the base
  2. Creates a Docker container to build a custom root filesystem
  3. Installs Podman, Nginx, and all system dependencies
  4. Captures running container images from the live dev server
  5. Bundles the frontend files, backend binary, and configs
  6. Writes a first-boot script that sets everything up on install
  7. Packages everything into a bootable ISO file

First Boot Sequence

When someone installs the ISO and boots for the first time, first-boot-containers.sh runs automatically and:

  1. Generates unique credentials for this installation (Bitcoin RPC password, database passwords)
  2. Sets up swap space based on available RAM
  3. Creates the archy-net container network for inter-container communication
  4. Starts 30+ containers in tiered order (databases first, then Bitcoin, then everything else)
  5. Runs health checks on critical containers
  6. Configures Tor hidden services

Quality Scores

After reviewing ~45,000 lines of Rust (213 files), ~45,500 lines of TypeScript/Vue (232 files), and ~40 shell scripts, here are the quality scores. Several scores improved since the last review thanks to major refactoring:

Rust Error Handling
A

Zero unwrap/panic in prod code

TypeScript Safety
A

Strict mode, zero any types

Security
A

33-finding pentest, all remediated

Frontend Architecture
A

God store split, god views split

Backend Modularity
A-

Monoliths split into subdirectories

Container Security
A

Cap-drop, readonly, non-root

Script Modularity
B-

Shared lib created, still large scripts

Test Coverage
B-

38 frontend + 36 backend test files

CI/CD
C

macOS release CI, no test gating

Documentation
A

This review + MASTER_PLAN + consolidated docs

Dependency Hygiene
B-

Floating crypto versions

Deploy Safety
A

Rollback, manifests, health checks, locking

Score improvements since last review (2026-03-20): Security A- → A (rate limiter backend, pentest complete), Frontend Architecture A- → A (god store + god views split), Backend Modularity B+ → A- (monolithic files → subdirectories), Script Modularity C+ → B- (shared library created), Documentation A- → A, Deploy Safety A- → A (deploy locking added).

What's Done Well

Rust: Exceptional Error Discipline

Zero unwrap() or panic!() in production code. Every fallible operation uses the ? operator to propagate errors gracefully. This is rare even in professional Rust codebases.

Backend Module Architecture (Refactored)

The backend was comprehensively refactored from monolithic files into domain-focused subdirectories. Previously: package.rs (1,795 lines), federation.rs (810 lines), handler.rs (800+ lines) were all single files. Now: each is a clean directory with focused sub-modules (e.g., package/ has config.rs, install.rs, lifecycle.rs, runtime.rs, stacks.rs, dependencies.rs, progress.rs). The RPC layer uses a dedicated dispatcher.rs for routing. All 8 major domains (package, federation, identity, mesh, system, tor, handler, credentials) follow the same mod.rs + handlers.rs pattern.

Frontend Component Decomposition (Refactored)

All "god components" were split into sub-views: Web5.vue (3,940 lines) → 14 focused sub-views under views/web5/. Settings.vue (1,792 lines) → 13 sections. Dashboard.vue, Apps.vue, AppDetails.vue, AppSession.vue, Federation.vue, Fleet.vue, Discover.vue — all extracted into subdirectories with focused components. The Pinia god store was decomposed into auth.ts, server.ts, and sync.ts.

Input Validation is Thorough

App IDs validated against a strict character whitelist. Container image names checked for shell injection characters. All external input sanitized at the boundary. Backend rate limiting on login + endpoints via new rate_limit.rs.

TypeScript Strict Mode Actually Used

All 5 strictest compiler flags enabled. Zero any types across 45,500+ lines. Every function has proper types. This prevents entire categories of bugs.

Container Security is Production-Grade

Every container drops all capabilities and adds back only what's needed. Read-only root filesystems. Non-root users. No-new-privileges. This is better than most commercial container platforms.

WebSocket Resilience

Auto-reconnection with exponential backoff, visibility change detection (handles tab switching), network online/offline detection. JSON patch (RFC 6902) for efficient incremental updates. The real-time connection is very robust.

Composables Well-Factored

11 Vue composables, each focused on one concern (toasts, audio, keyboard, onboarding, controller nav). Clean, reusable, properly scoped. 10 test files for composables.

Deploy Safety Features

Rollback backups before deployment, deploy manifests tracking what was deployed, health checks after deployment, progress bars with ETAs. Deploy locking prevents concurrent deploys. Shared script library (scripts/lib/common.sh) eliminates function duplication.

Monitoring & Telemetry System

New monitoring/ module (1,380 LOC) with metrics collection, alert generation, persistent storage, beta telemetry reporting, and notification dispatch. Production-grade observability for the beta phase.

PodmanClient Uses REST API Socket

The container management layer communicates with Podman via its async REST API unix socket (/run/user/{UID}/podman/podman.sock), not CLI. This is faster, more reliable, and avoids shell injection risks.

Full Security Audit Completed

A comprehensive penetration test (33 findings) was completed in March 2026 and all findings were remediated. Security rules from findings are enforced in CLAUDE.md for all future code.

What Needs Fixing

Production Reliability P0 — blocks beta

P0-1. Health RPC endpoint has no handler

What: "health" is listed in UNAUTHENTICATED_METHODS but has no match handler — returns "Unknown method" error instead of actual health status.

Impact: Frontend, load balancers, and orchestrators can't verify the backend is actually healthy. System appears unhealthy when it's fine.

Fix: Add handler that checks crash recovery status, Podman responsiveness, and service readiness.

P0-2. Zero container health checks across all 30 containers

What: first-boot-containers.sh creates 30+ containers with --restart unless-stopped but zero --health-cmd flags. Crashed containers restart endlessly in a hammer loop.

Impact: Silent failures — a broken app looks "running" but returns errors. No way for the backend to distinguish healthy from crashed.

Fix: Add --health-cmd with appropriate checks (HTTP, TCP, CLI) to every container.

P0-3. Backup restore has no pre-validation or atomic rollback

What: restore_full_backup() extracts directly to the live data directory. If extraction fails halfway, the system is left in a corrupt partial state with no way to recover.

Impact: A corrupted backup can brick a fresh install. Data loss on partial restore failure.

Fix: Extract to staging directory, validate required files, atomic rename, rollback on failure.

P0-4. Unauthenticated nginx endpoints missing protections

What: /archipelago/, /content, /dwn endpoints (used for Tor P2P federation) have no timeout, body size limit, or rate limiting.

Impact: Vulnerable to slow-loris attacks, payload flooding, and connection exhaustion via Tor.

Fix: Add proxy_connect_timeout, client_max_body_size 10m, and limit_req to all three locations.

Critical Issues recently resolved

RESOLVED: package.rs was 1,795 lines — split into 7 files

Before: Single monolithic file handling all container operations.

After: Split into package/config.rs (692 LOC), package/install.rs (467 LOC), package/lifecycle.rs, package/runtime.rs (417 LOC), package/stacks.rs (356 LOC), package/dependencies.rs (242 LOC), package/progress.rs (140 LOC). Each file has one clear responsibility.

RESOLVED: Web5.vue was 3,940 lines — split into 14 sub-views

Before: One massive component with 17 sections.

After: Extracted to views/web5/ with: Web5.vue (main), Web5ConnectedNodes, Web5CredentialsSummary, Web5DWN, Web5Domains, Web5Identities, Web5NodeVisibility, Web5NostrRelays, Web5QuickActions, Web5SendReceiveModals, Web5SharedContent, Web5Wallet, types.ts, utils.ts.

RESOLVED: useAppStore was a "god store" — split into 3 focused stores

Before: One store handling auth, WebSocket, server data, and package management.

After: Decomposed into auth.ts (login/logout/TOTP/sessions), server.ts (server state + RPC actions), sync.ts (WebSocket + JSON patch data sync). app.ts is now a thin data store.

RESOLVED: Shell scripts had no shared library

Before: Duplicated functions across deploy, first-boot, and helper scripts.

After: scripts/lib/common.sh provides shared functions: colored logging, SSH wrappers (ssh_cmd, scp_cmd), health checks, disk checks, memory limits. Sourced by all deployment scripts.

Remaining Critical Issues fix now

1. Test coverage exists but has gaps

What: 38 frontend test files and 36+ backend test modules exist. However, coverage is uneven — critical paths like session validation, federation sync, and the app install flow lack thorough test suites.

Fix: Add integration tests for critical paths (auth flow, container lifecycle, federation handshake). Add CI that runs cargo test + npm test on every push.

High Priority fix soon

P1-A. Nostr client.connect() hangs indefinitely (no timeout)

What: 4 calls to client.connect().await in nostr_handshake.rs have no timeout wrapper. If a relay is down, peer discovery hangs forever.

Fix: Wrap all in tokio::time::timeout(Duration::from_secs(10), ...).

P1-B. Rate limiter memory grows unbounded

What: EndpointRateLimiter::cleanup() and LoginRateLimiter cleanup methods exist but are never spawned. HashMap of (method, IP) entries grows forever.

Fix: Spawn cleanup task every 5 minutes in RpcHandler::new().

P1-C. Systemd service missing resource limits

What: No MemoryMax, LimitNOFILE, or TasksMax in archipelago.service. A memory leak in the backend can OOM-kill the entire system.

Fix: Add MemoryMax=4G, LimitNOFILE=65535, TasksMax=2048.

P1-D. Container images using :latest tag (7 instances)

What: Several containers in first-boot-containers.sh and the ISO build pull :latest — no version pinning.

Impact: Two machines installed a week apart may have different Bitcoin node versions. Supply chain risk.

Fix: Pin every image to a specific version tag or SHA256 digest.

P1-E. WebSocket reconnect doesn't refresh full state

What: After a WebSocket disconnect (5+ minutes), the UI shows stale data. Reconnection applies patches to an outdated base state instead of fetching fresh data.

Fix: On reconnect, call server.get-state RPC to refresh full state before accepting patches.

P1-F. No global Vue error handler

What: No app.config.errorHandler in main.ts. Component errors silently log to console — user sees blank screen with no recovery path.

Fix: Add error handler that shows user-visible toast and logs structured error.

5. Cryptographic dependency versions not pinned exactly

What: zeroize = "1.7", chacha20poly1305 = "0.10", ed25519-dalek = "2.1" use floating versions.

Why it's bad: A minor version bump in a crypto library could introduce a vulnerability or behavioral change. The project's own rules require exact pinning for crypto deps.

Fix: Pin to exact versions: "1.7.0", "0.10.1", "2.1.1".

6. No frontend-backend type synchronization

What: TypeScript types in types/api.ts are manually maintained copies of Rust structs. If the backend changes a field name, the frontend doesn't know until runtime.

Why it's bad: Types can drift apart silently. A backend developer renames sync_progress to syncProgress and the frontend breaks in production.

Fix: Generate TypeScript types from Rust structs (using ts-rs or a JSON Schema).

7. Container metadata duplicated in 3 places

What: App configuration (ports, volumes, env vars) exists in: package.rs (RPC handler), docker_packages.rs (metadata reader), health_monitor.rs (startup tiers).

Why it's bad: Adding a new app means updating 3 files. If you forget one, the app partially works but something is wrong.

Fix: Single app config source (manifest YAML or a shared Rust module) that all three consumers read from.

8. Deploy and ISO build scripts are still 1,700+ lines each

What: Two monolithic shell scripts (deploy: ~1,790 lines, ISO build: ~1,870 lines) handle dozens of responsibilities each. Shared functions have been extracted to scripts/lib/common.sh, but the scripts themselves are still large.

Improvement: scripts/lib/common.sh now provides shared logging, SSH wrappers, health checks, and memory limits — eliminating most duplication. But the core scripts could still benefit from modular splitting post-beta.

Next step: Split deploy into modules: deploy-frontend.sh, deploy-backend.sh, sync-configs.sh. Split ISO build into lib/rootfs.sh, lib/components.sh, lib/installer-env.sh.

Medium Priority improve over time

9. App integration requires updates in 6+ locations

What: Adding a new app to Archipelago requires manual changes in: manifest YAML, package.rs (backend config), docker_packages.rs (metadata), nginx config (routing), Marketplace.vue (frontend listing), appLauncher.ts (port mapping), first-boot-containers.sh (first boot), build-auto-installer-iso.sh (ISO capture).

Fix: Move toward a single manifest file per app that drives all of these automatically.

10. CI/CD pipeline is minimal

What: One GitHub Action builds macOS release binaries on tag push. No tests run in CI. No linting. No Linux build or deploy automation.

Fix: Add CI that runs cargo clippy, cargo test, npm run type-check, and npm run lint on every push. Add Linux cross-compilation.

11. Session persistence uses blocking I/O

What: On startup, session.rs reads sessions.json using synchronous (blocking) file I/O in an async context.

Fix: Use tokio::fs::read_to_string for non-blocking I/O at startup.

12. Inconsistent loading state patterns in frontend

What: Some components use loading, others isLoading, others loadingApps. No shared composable.

Fix: Create a useAsyncState composable that standardizes loading/error/data patterns.

Refactoring Priorities

Ordered by impact. 6 of 12 items completed since the previous review — significant progress:

#TaskImpactEffortStatus
1 Split package.rs (1,795 lines) into focused files high 2-3 days DONE
2 Split useAppStore into auth/server/sync high 2 days DONE
3 Add CI pipeline (clippy + type-check + basic tests) high 1 day TODO
4 Split Web5.vue (3,940 lines) into sub-views medium 3 days DONE
5 Pin all crypto dependency versions exactly medium 1 hour TODO
6 Extract shared shell library (lib/common.sh) medium 1 day DONE
7 Consolidate container metadata to single source medium 2 days TODO
8 Generate TypeScript types from Rust structs medium 1 day TODO
9 Split deploy/ISO scripts into modules low 2 days POST-BETA
10 Add integration tests for critical paths high 3 days TODO
11 Split large backend files (federation, identity, handler, system, tor) medium 2 days DONE
12 Split large Vue views (Settings 1,792, Mesh, Dashboard, Apps, etc.) low 2 days DONE

Technical Debt Map

A visual summary of where debt lives in the codebase. Many red items from the previous review are now green after refactoring:

BACKEND (Rust) — 213 files, ~45K LOC ████████ package/ (2,248 lines in 7 focused files — was 1,795 god file) ████████ api/handler/ (896 lines in 6 files — was 800+ god file) ██████ federation/, identity/, system/, tor/ (all split from monoliths) ██████ credentials/ (5 files), monitoring/ (7 files) — new modules ██████ lnd/ (1,092 lines in 5 files — could split further) ████ mesh/ (6,000 lines — large but domain-appropriate) ████ session.rs (622), health_monitor.rs (731) — clean single files ████ rate_limit.rs (191) — new, focused ██ container, security, performance crates (clean) FRONTEND (Vue + TS) — 232 files, ~45.5K LOC ████████ web5/ (14 sub-views — was 3,940-line god component) ████████ settings/ (13 sections — was 1,792-line god component) ██████ apps/, dashboard/, federation/, fleet/, discover/ (all split) ████ auth.ts + server.ts + sync.ts (was god store) ██ rpc-client.ts (well-designed), 11 composables (clean) Type safety (excellent), 38+ test files SCRIPTS (Shell) — ~40 scripts ████████████ deploy-to-target.sh (~1,790 lines — still large) ████████████ build-auto-installer-iso.sh (~1,870 lines — still large) ██████ first-boot-containers.sh (~935 lines, version mismatches) ████ scripts/lib/common.sh — shared library (new) ████ image-versions.sh — centralized pinning (new) ██ Test scripts (well-organized) ARCHITECTURE ██████ Tests: 74+ files but gaps in integration coverage ██████ CI limited to macOS release builds — no test gating ████ Manual type sync (Rust ↔ TypeScript) ████ App integration requires 6+ file changes ████ Crypto deps use floating versions ████ Security model (pentest completed, rate limiting, CSRF) ████ Deploy safety (rollback, manifests, locking, health checks) ████ Module architecture (all god files eliminated) ██ PodmanClient (REST API socket, not CLI) ██ Monitoring & telemetry system (production-ready) Legend: ██ Critical ██ Needs attention ██ Good Progress: ██████████████████████████████████████████ ~75% green (was ~40%)

Recommended Learning Path

If you want to understand this codebase deeply and become proficient in all the technologies, study in this order:

Phase 1: Foundations (Weeks 1-4)

  1. Linux basics — commands, file permissions, processes, systemd
  2. Git — branches, commits, diffs, rebasing
  3. HTML/CSS/JavaScript — the building blocks of web UIs
  4. TypeScript — JavaScript with type safety (read the official handbook)

Phase 2: Frontend (Weeks 5-8)

  1. Vue 3 Composition APIref, computed, watch, onMounted
  2. Pinia — state management (read stores/container.ts as a good example)
  3. Vue Router — URL-to-component mapping
  4. Tailwind CSS — utility-first CSS framework
  5. Vite — the build tool that bundles everything

Phase 3: Backend (Weeks 9-14)

  1. Rust basics — ownership, borrowing, lifetimes, pattern matching (read "The Rust Book")
  2. Async Rust with Tokioasync/await, futures, tokio::spawn
  3. Hyper — the HTTP server library (read server.rs)
  4. Serde — JSON serialization/deserialization
  5. Error handlinganyhow, thiserror, the ? operator

Phase 4: Infrastructure (Weeks 15-18)

  1. Containers — Docker/Podman concepts (images, containers, volumes, networks)
  2. Nginx — reverse proxy, location blocks, upstream servers
  3. Shell scripting — bash/zsh, set -e, functions, trap
  4. systemd — service management, unit files, journalctl
  5. Networking — TCP/IP, DNS, ports, firewalls (UFW)

Phase 5: Bitcoin & Crypto (Weeks 19-24)

  1. Bitcoin protocol — blocks, transactions, UTXOs, mining (read "Mastering Bitcoin")
  2. Lightning Network — payment channels, routing, invoices
  3. Cryptography — hashing, symmetric/asymmetric encryption, digital signatures
  4. Tor — onion routing, hidden services, SOCKS5 proxy
  5. Nostr — decentralized messaging protocol, NIPs
  6. DIDs — Decentralized Identifiers, Verifiable Credentials
Recommended first files to read
  1. neode-ui/src/stores/auth.ts — Clean authentication state machine (new, focused store)
  2. neode-ui/src/stores/sync.ts — WebSocket + JSON patch data sync (new)
  3. neode-ui/src/api/rpc-client.ts — Well-designed API client with retry logic
  4. core/archipelago/src/api/rpc/dispatcher.rs — How RPC routing works (new)
  5. core/archipelago/src/api/rpc/package/install.rs — App install flow (focused)
  6. core/archipelago/src/session.rs — Auth flow in Rust with crypto
  7. core/container/src/podman_client.rs — How Rust talks to Podman
  8. image-recipe/configs/nginx-archipelago.conf — The full routing map

Glossary

TermWhat It Means
APIApplication Programming Interface — a defined way for two programs to talk to each other
Async/AwaitA way to write code that waits for slow things (network, disk) without blocking other work
BackendThe server-side code that runs on the machine (not visible to users)
ContainerAn isolated environment for running an app, like a lightweight virtual machine
ComposableA reusable piece of logic in Vue (similar to React hooks)
CSRFCross-Site Request Forgery — an attack where a malicious site tricks your browser into sending requests
CrateA Rust package (like npm package for JavaScript)
DIDDecentralized Identifier — a self-owned digital identity (no central authority controls it)
DWNDecentralized Web Node — personal data storage that syncs across your devices
FrontendThe browser-side code that users see and interact with
ISOA disk image file — like a digital copy of an installation CD
JWTJSON Web Token — a compact way to pass verified identity between systems
LoRaLong Range radio — low-power wireless communication over several kilometers
NginxA web server that also works as a reverse proxy (routes traffic to the right service)
NostrA decentralized messaging protocol using public/private key pairs
Onion ServiceA Tor hidden service — a server accessible only through the Tor network (no IP address)
PiniaVue's official state management library (successor to Vuex)
PodmanA container runtime like Docker, but rootless (more secure)
RPCRemote Procedure Call — calling a function on another computer over the network
ReactiveData that automatically updates the UI when it changes (core Vue concept)
Reverse ProxyA server that sits between clients and backend servers, forwarding requests
RustA systems programming language focused on safety and performance
SPASingle Page Application — a web app that loads once and dynamically updates content
Satoshi (sat)The smallest unit of Bitcoin. 1 BTC = 100,000,000 sats
systemdLinux's service manager — starts, stops, and monitors background services
TokioRust's async runtime — handles thousands of concurrent operations efficiently
TorThe Onion Router — anonymizes internet traffic by routing through multiple relays
TypeScriptJavaScript with static types — catches bugs at compile time instead of runtime
Vue 3A JavaScript framework for building reactive user interfaces
WebSocketA persistent, two-way connection between browser and server for real-time data

Architecture Review — Archipelago v0.1.0-beta — Updated 2026-03-22
~45,000 lines Rust (213 files) · ~45,500 lines TypeScript/Vue (232 files) · ~40 shell scripts