Linux boots — Debian 12 starts up, loads drivers, mounts disks
Archipelago
A complete architecture review and learning guide for the Bitcoin Node OS — explained so anyone can understand it.
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:
- A full Bitcoin node — you verify your own transactions, no trust in anyone else
- A Lightning Network node — fast, cheap Bitcoin payments
- A web dashboard — manage everything from your phone or laptop browser
- An app marketplace — install apps like Nextcloud, Jellyfin, Vaultwarden with one click
- Privacy by default — Tor routing, encrypted secrets, no telemetry
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:
How It Runs on a Machine
When you install Archipelago on a computer and power it on, here's what happens in order:
systemd starts services — A program called systemd reads archipelago.service and launches the Rust backend
Rust backend initializes — Loads config, creates/loads encryption keys, starts the HTTP server on port 5678
Health monitor starts — Checks which containers are running, restarts crashed ones, reports readiness
Nginx starts — Listens on port 80 (HTTP) and routes all incoming traffic
Containers start — Bitcoin, LND, and other apps start in priority order (Bitcoin first, then things that depend on it)
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.
How the code is organized
The Rust code lives in core/ and is split into 5 separate packages (called "crates"):
| Crate | What It Does | Size | Analogy |
|---|---|---|---|
archipelago |
The main program. Contains all the API endpoints, authentication, identity management, federation, mesh networking | ~12,000 lines | The restaurant manager — coordinates everything |
container |
Talks to Podman to create, start, stop, and monitor containers | ~2,000 lines | The kitchen manager — controls the cook stations |
security |
Encrypts secrets, generates security profiles, verifies container images | ~500 lines | The security guard — locks doors, checks IDs |
performance |
Monitors CPU, memory, and disk usage | ~300 lines | The meter reader — watches resource gauges |
parmanode |
Compatibility layer for migrating from an older project | ~600 lines | A translation book — speaks the old language |
Key files you should know
| File | What It Does | Lines |
|---|---|---|
main.rs | The entry point — starts the server, registers signal handlers | ~200 |
server.rs | Wires everything together — creates the HTTP server, connects components | ~500 |
api/rpc/mod.rs | The traffic cop — receives API calls and sends them to the right handler | ~1,000 |
api/rpc/package.rs | The app installer — handles installing, starting, stopping containers | ~1,770 |
session.rs | Login management — creates sessions, validates tokens, persists to disk | ~790 |
health_monitor.rs | Watches containers, restarts crashed ones, reports system health | ~710 |
federation.rs | Multi-node communication — syncs state between trusted Archipelago nodes | ~810 |
credentials.rs | Verifiable credentials — W3C standard digital identity proofs | ~800 |
How the backend handles a request
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.
Frontend file structure
neode-ui/src/
├── api/ ← How the frontend talks to the backend
│ ├── rpc-client.ts ← Makes API calls (fetch + retry + auth)
│ ├── container-client.ts ← Container-specific API helpers
│ └── websocket.ts ← Real-time updates (push, not poll)
├── views/ ← Full pages (one per route)
│ ├── Dashboard.vue ← Main dashboard with sidebar
│ ├── Marketplace.vue ← App store for installing containers
│ ├── Settings.vue ← System settings
│ ├── Web5.vue ← Decentralized identity management
│ ├── Mesh.vue ← LoRa mesh radio interface
│ └── Login.vue ← Login page
├── components/ ← Reusable UI pieces
│ ├── BootScreen.vue ← Startup loading animation
│ ├── SplashScreen.vue ← Welcome/intro screen
│ └── SpotlightSearch.vue ← Command palette (Cmd+K)
├── stores/ ← State management (Pinia)
│ ├── app.ts ← Core app state (auth, server data)
│ ├── container.ts ← Container states & lifecycle
│ ├── mesh.ts ← Mesh networking state
│ └── appLauncher.ts ← App launching & iframe management
├── composables/ ← Reusable logic (like React hooks)
│ ├── useToast.ts ← Notification popups
│ └── useAudioPlayer.ts ← Sound effects
├── types/ ← TypeScript type definitions
│ └── api.ts ← Shapes of data from the backend
├── router/ ← URL → page mapping
└── style.css ← All global styles (glassmorphism theme)
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.
Archipelago uses Podman instead of Docker. They're nearly identical, but Podman runs without root privileges (more secure) and doesn't need a background daemon.
Container security rules
Every container in Archipelago follows strict security rules:
| Rule | What It Means | Why |
|---|---|---|
--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)
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
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 Pattern | Backend | Rate Limit | Timeout | Purpose |
|---|---|---|---|---|
/rpc/ | :5678 | 20r/s (burst 40) | 600s | All RPC API calls (1MB body limit) |
/ws | :5678 | — | 86,400s (24h) | WebSocket — real-time state updates |
/health | :5678 | — | default | Health check (no auth) |
/archipelago/ | :5678 | — | default | System endpoints |
/content | :5678 | — | default | Peer content sharing |
/dwn | :5678 | — | default | Decentralized Web Node |
/electrs-status | :5678 | — | default | Electrum sync status (CORS enabled) |
/lnd-connect-info | :5678 | — | default | LND 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.
| App | Port | Special Config |
|---|---|---|
bitcoin-ui | 8334 | — |
mempool | 4080 | 300s timeouts |
lnd | 8081 | 300s timeouts |
electrumx | 50002 | — |
btcpay | 23000 | — |
fedimint | 8175 | 300s timeouts |
fedimint-gateway | 8176 | 300s timeouts |
filebrowser | 8083 | 10GB uploads, path traversal blocking |
nextcloud | 8085 | 300s timeouts |
vaultwarden | 8082 | — |
immich | 2283 | 300s timeouts |
jellyfin | 8096 | — |
grafana | 3000 | — |
portainer | 9000 | — |
uptime-kuma | 3001 | — |
searxng | 8888 | — |
ollama | 11434 | — |
indeedhub | 7777 | URL rewriting, WS, 30-day asset cache |
homeassistant | 8123 | 86,400s timeout (persistent) |
penpot | 9001 | 300s timeouts |
photoprism | 2342 | — |
onlyoffice | 8044 | — |
endurain | 8080 | — |
nginx-proxy-manager | 8181 | — |
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 Pattern | Backend | Timeout | Purpose |
|---|---|---|---|
/aiui/ | Static files | — | Chat UI (no-cache for HTML) |
/aiui/api/claude/ | :3142 | 300s read | Claude proxy (streaming, no buffering) |
/aiui/api/ollama/ | :11434 | 300s read | Local Ollama model (streaming) |
/aiui/api/openrouter/ | openrouter.ai | 120s | External AI API (SSL passthrough) |
/aiui/api/web-search | :8888 | 30s | SearXNG 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 |
Unique Nginx Features in Archipelago
Nostr NIP-07 Injection
- Every app proxy uses
sub_filterto injectnostr-provider.jsinto</head> - Gives all container apps
window.nostrfor signing - No other node OS does this — unique to Archipelago
Accept-Encodingdisabled 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/srcattributes 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 offfor 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 withimmutable- WebSocket at
/app/indeedhub/ws/with 24h timeout - Rewrites both single and double quoted
href/src
Nginx Config File Map
| File | Lines | Purpose |
|---|---|---|
image-recipe/configs/nginx-archipelago.conf | ~1,100 | Production config — HTTP + HTTPS servers, all routing |
image-recipe/configs/snippets/archipelago-https-app-proxies.conf | ~400 | HTTPS app proxy blocks (included in main config) |
image-recipe/configs/snippets/archipelago-pwa.conf | ~30 | PWA service worker and manifest caching |
image-recipe/configs/external-app-proxies.conf | ~200 | External site reverse proxies (BotFights, 484 Kitchen) |
neode-ui/docker/nginx.conf | ~60 | Dev Docker config (mock backend on :5959) |
neode-ui/docker/nginx-demo.conf | ~80 | Demo mode config (no security, mock backend) |
docker/bitcoin-ui/nginx.conf | ~50 | Bitcoin UI container — RPC proxy with CORS |
docker/electrs-ui/nginx.conf | ~30 | Electrs UI container — status endpoint |
docker/lnd-ui/nginx.conf | ~30 | LND UI container — connect info |
indeedhub/nginx.conf | ~200 | IndeedhHub container — MinIO, API, relay, SPA |
How Data Flows Through the System
Let's trace what happens when you click "Install Bitcoin" in the UI:
You click the Install button in Marketplace.vue. Vue calls the Pinia store action installPackage('bitcoin-knots')
The store calls the RPC client: rpcClient.installPackage('bitcoin-knots', 'docker.io/bitcoin/knots:28')
RPC client sends HTTP POST to /rpc/v1 with a session cookie and CSRF token for security
Nginx receives the request on port 80, checks rate limits, forwards to the Rust backend on port 5678
Rust backend validates — checks your session is valid, CSRF token matches, app ID is safe (no shell injection characters)
Rust checks dependencies — if you're installing LND, it checks Bitcoin is already running
Rust tells Podman to pull the image — podman pull docker.io/bitcoin/knots:28 (downloads the app)
Rust creates and starts the container with all security flags (cap-drop, readonly root, etc.)
Backend sends a WebSocket update — the frontend receives a "state changed" event in real time
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.
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 Method | Backend Handler | What It Does |
|---|---|---|
rpcClient.login(password) | auth.login | Log in with password |
rpcClient.getServerInfo() | system.info | Get server name, version, uptime |
rpcClient.installPackage(id, image) | package.install | Install a container app |
rpcClient.getBitcoinInfo() | bitcoin.info | Get blockchain sync %, block height |
rpcClient.sendMeshMessage(text) | mesh.send | Send a message over LoRa radio |
Built-in resilience
The RPC client has built-in protections:
- Auto-retry — if a request fails (502/503), it waits and tries again (up to 3 times)
- Timeout — if the backend doesn't respond in 30 seconds, the request fails instead of hanging forever
- Session expiry — if you get a 401 (unauthorized), it redirects to the login page
- CSRF protection — every request includes a security token to prevent cross-site attacks
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.
Archipelago has 15 Pinia stores:
app.ts god store
Auth, WebSocket, server data, package management — does too much (see refactoring section)
container.ts good
Container lifecycle — running, stopped, installing states
mesh.ts okay
LoRa radio state — device, peers, messages, channels
appLauncher.ts okay
App iframe management, Nostr consent, port mapping
spotlight.ts good
Command palette (Cmd+K) — search, help modal
goals.ts good
Gamified goal/quest tracking state machine
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.
Authentication & Sessions
When you log in, the backend creates a session — a temporary "you're allowed in" token. Here's how it works:
You enter your password on the login page
Backend hashes it with bcrypt — a one-way function that makes it impossible to reverse
Backend compares the hash to the stored hash (never compares raw passwords)
Backend creates a session — generates a random 256-bit token using a cryptographically secure random number generator
Session ID sent as a cookie — the browser stores it and sends it with every request
CSRF token also sent — a second token that prevents cross-site request forgery attacks
Security Model
Archipelago is a defense-in-depth system — multiple layers of security so that if one fails, others still protect you.
| Layer | Protection | Against 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.
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.
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,570-line shell script that automates everything:
Pre-flight checks — verifies SSH connectivity, checks git state, warns about uncommitted changes
Frontend build — runs npm run build to compile Vue/TypeScript into static files
Upload frontend — rsyncs built files to /opt/archipelago/web-ui/ on the server
Upload Rust source — rsyncs core/ to the server (builds ON the server, not macOS)
Build on server — runs cargo build --release on the Linux server
Sync configs — copies nginx config, systemd service from image-recipe/configs/
Restart services — reloads nginx, restarts the Rust backend via systemd
Health check — pings /health endpoint to verify everything came back up
Deploy manifest — writes a JSON file recording the commit, timestamp, and deploy status
ISO Build Process
The ISO build creates the installer that users flash to USB. It's a 1,775-line script that:
- Downloads a Debian 12 Live ISO as the base
- Creates a Docker container to build a custom root filesystem
- Installs Podman, Nginx, and all system dependencies
- Captures running container images from the live dev server
- Bundles the frontend files, backend binary, and configs
- Writes a first-boot script that sets everything up on install
- 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:
- Generates unique credentials for this installation (Bitcoin RPC password, database passwords)
- Sets up swap space based on available RAM
- Creates the
archy-netcontainer network for inter-container communication - Starts 30+ containers in tiered order (databases first, then Bitcoin, then everything else)
- Runs health checks on critical containers
- Configures Tor hidden services
Quality Scores
After reviewing ~46,000 lines of Rust, ~12,000 lines of TypeScript, and ~100 shell scripts, here are the quality scores:
Zero unwrap/panic in prod code
Strict mode, zero any types
Defense in depth, minor gaps
Well-organized, 1 god store
Good separation, large files
Cap-drop, readonly, non-root
Monolithic, no shared library
No automated tests
Build only, no test gating
Good docs, gaps in API ref
Floating crypto versions
Rollback, manifests, health checks
What's Done Well
Rust: Exceptional Error Discipline
Zero unwrap() or panic!() in production code (only 2 expect() in startup code). Every fallible operation uses the ? operator to propagate errors gracefully. This is rare even in professional Rust codebases.
Input Validation is Thorough
App IDs validated against a strict character whitelist. Docker image names checked for shell injection characters. All external input sanitized at the boundary.
TypeScript Strict Mode Actually Used
All 5 strictest compiler flags enabled. Zero any types across 12,000+ 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. The real-time connection is very robust.
Composables Well-Factored
11 Vue composables, each focused on one concern (toasts, audio, keyboard, onboarding). Clean, reusable, properly scoped.
Deploy Safety Features
Rollback backups before deployment, deploy manifests tracking what was deployed, health checks after deployment, progress bars with ETAs.
What Needs Fixing
Critical Issues fix now
1. package.rs is 1,770 lines — a "god file"
What: core/archipelago/src/api/rpc/package.rs handles ALL container operations: install, start, stop, configure ports, configure volumes, configure environment variables, dependency checking, image validation, progress streaming.
Why it's bad: You can't change one thing without risking breaking something else. It's impossible to test in isolation. Any new app requires modifying this massive file.
Fix: Split into app_config.rs (port/volume/env definitions), app_lifecycle.rs (install/start/stop), app_validation.rs (input checks, dependency verification).
2. Web5.vue is 3,901 lines — a "god component"
What: One Vue file contains 17 different sections: DID management, wallet, Nostr relays, credentials, voting, P2P peers, storage, profiles, marketplace, goals, data explorer, and more.
Why it's bad: Loading one massive component is slow. Changes to the voting section could break the wallet section. Impossible to reuse any section independently.
Fix: Split into 5+ sub-views under /dashboard/web5/ with their own routes.
3. No automated tests
What: Zero unit tests in the Rust backend. No integration tests. No end-to-end tests that run automatically. The only "test" is deploying and checking manually.
Why it's bad: Every change could break something, and you won't know until a user reports it. As the codebase grows, confidence in changes decreases.
Fix: Start with tests for the most critical paths: session validation, input sanitization, container lifecycle. Add CI that runs tests on every push.
4. useAppStore is a "god store" with 8+ responsibilities
What: One Pinia store handles: auth state, WebSocket connection, server data, package install/uninstall, server restart/shutdown, marketplace data, metrics, loading states.
Why it's bad: Every component that imports this store gets ALL of its complexity. Hard to track where state changes come from. Testing any one concern requires mocking everything else.
Fix: Split into auth.ts, server.ts, realtime.ts, keep app.ts as a thin data store only.
High Priority fix soon
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 1,500+ lines each
What: Two monolithic shell scripts handle dozens of responsibilities each, with duplicated utility functions across 15+ scripts.
Why it's bad: Hard to review, hard to debug, hard to modify. One wrong change can break the entire deploy pipeline. No shared library means the same health-check loop is copy-pasted in 8 places.
Fix: Extract shared functions into scripts/lib/common.sh. Split deploy into modules: deploy-frontend.sh, deploy-backend.sh, sync-configs.sh, health-checks.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. No CI/CD pipeline
What: One GitHub Action builds a macOS binary. No tests run. No linting. No deploy automation.
Fix: Add CI that runs cargo clippy, cargo test, npm run type-check, and npm run lint on every push.
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 — what to fix first for the biggest improvement:
| # | Task | Impact | Effort | Priority |
|---|---|---|---|---|
| 1 | Split package.rs into 3-4 focused files |
high | 2-3 days | P0 |
| 2 | Split useAppStore into auth/server/realtime |
high | 2 days | P0 |
| 3 | Add CI pipeline (clippy + type-check + basic tests) | high | 1 day | P0 |
| 4 | Split Web5.vue into sub-views |
medium | 3 days | P1 |
| 5 | Pin all crypto dependency versions exactly | medium | 1 hour | P1 |
| 6 | Extract shared shell library (lib/common.sh) |
medium | 1 day | P1 |
| 7 | Consolidate container metadata to single source | medium | 2 days | P1 |
| 8 | Generate TypeScript types from Rust structs | medium | 1 day | P2 |
| 9 | Split deploy script into modules | low | 2 days | P2 |
| 10 | Add unit tests for critical paths (session, validation) | high | 3 days | P2 |
| 11 | Create useAsyncState composable for frontend |
low | 4 hours | P3 |
| 12 | Split large Vue components (SplashScreen, Mesh, Settings) | low | 2 days | P3 |
Technical Debt Map
A visual summary of where debt lives in the codebase:
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)
- Linux basics — commands, file permissions, processes, systemd
- Git — branches, commits, diffs, rebasing
- HTML/CSS/JavaScript — the building blocks of web UIs
- TypeScript — JavaScript with type safety (read the official handbook)
Phase 2: Frontend (Weeks 5-8)
- Vue 3 Composition API —
ref,computed,watch,onMounted - Pinia — state management (read
stores/container.tsas a good example) - Vue Router — URL-to-component mapping
- Tailwind CSS — utility-first CSS framework
- Vite — the build tool that bundles everything
Phase 3: Backend (Weeks 9-14)
- Rust basics — ownership, borrowing, lifetimes, pattern matching (read "The Rust Book")
- Async Rust with Tokio —
async/await, futures,tokio::spawn - Hyper — the HTTP server library (read
server.rs) - Serde — JSON serialization/deserialization
- Error handling —
anyhow,thiserror, the?operator
Phase 4: Infrastructure (Weeks 15-18)
- Containers — Docker/Podman concepts (images, containers, volumes, networks)
- Nginx — reverse proxy, location blocks, upstream servers
- Shell scripting — bash/zsh,
set -e, functions, trap - systemd — service management, unit files, journalctl
- Networking — TCP/IP, DNS, ports, firewalls (UFW)
Phase 5: Bitcoin & Crypto (Weeks 19-24)
- Bitcoin protocol — blocks, transactions, UTXOs, mining (read "Mastering Bitcoin")
- Lightning Network — payment channels, routing, invoices
- Cryptography — hashing, symmetric/asymmetric encryption, digital signatures
- Tor — onion routing, hidden services, SOCKS5 proxy
- Nostr — decentralized messaging protocol, NIPs
- DIDs — Decentralized Identifiers, Verifiable Credentials
neode-ui/src/stores/container.ts— Clean, well-structured Pinia store (312 lines)neode-ui/src/api/rpc-client.ts— Well-designed API client with retry logiccore/archipelago/src/session.rs— Auth flow in Rust with cryptocore/container/src/podman_client.rs— How Rust talks to Podmanimage-recipe/configs/nginx-archipelago.conf— The full routing map
Glossary
| Term | What It Means |
|---|---|
| API | Application Programming Interface — a defined way for two programs to talk to each other |
| Async/Await | A way to write code that waits for slow things (network, disk) without blocking other work |
| Backend | The server-side code that runs on the machine (not visible to users) |
| Container | An isolated environment for running an app, like a lightweight virtual machine |
| Composable | A reusable piece of logic in Vue (similar to React hooks) |
| CSRF | Cross-Site Request Forgery — an attack where a malicious site tricks your browser into sending requests |
| Crate | A Rust package (like npm package for JavaScript) |
| DID | Decentralized Identifier — a self-owned digital identity (no central authority controls it) |
| DWN | Decentralized Web Node — personal data storage that syncs across your devices |
| Frontend | The browser-side code that users see and interact with |
| ISO | A disk image file — like a digital copy of an installation CD |
| JWT | JSON Web Token — a compact way to pass verified identity between systems |
| LoRa | Long Range radio — low-power wireless communication over several kilometers |
| Nginx | A web server that also works as a reverse proxy (routes traffic to the right service) |
| Nostr | A decentralized messaging protocol using public/private key pairs |
| Onion Service | A Tor hidden service — a server accessible only through the Tor network (no IP address) |
| Pinia | Vue's official state management library (successor to Vuex) |
| Podman | A container runtime like Docker, but rootless (more secure) |
| RPC | Remote Procedure Call — calling a function on another computer over the network |
| Reactive | Data that automatically updates the UI when it changes (core Vue concept) |
| Reverse Proxy | A server that sits between clients and backend servers, forwarding requests |
| Rust | A systems programming language focused on safety and performance |
| SPA | Single 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 |
| systemd | Linux's service manager — starts, stops, and monitors background services |
| Tokio | Rust's async runtime — handles thousands of concurrent operations efficiently |
| Tor | The Onion Router — anonymizes internet traffic by routing through multiple relays |
| TypeScript | JavaScript with static types — catches bugs at compile time instead of runtime |
| Vue 3 | A JavaScript framework for building reactive user interfaces |
| WebSocket | A persistent, two-way connection between browser and server for real-time data |
Architecture Review — Archipelago v0.1.0-alpha — Generated 2026-03-18
~46,000 lines Rust · ~12,000 lines TypeScript · ~100 shell scripts