Files
archy/docs/architecture-review.html
Dorian 0e0c97c203 feat: architecture review fixes, self-update system, CI pipeline, supply chain hardening
Architecture review (all P0+P1 issues now fixed):
- Add 10s timeout to 6 bare Nostr client.connect() calls
- Pin all 12 crypto deps to exact versions from Cargo.lock
- Pin all 15 floating container image tags to exact patch versions
- Add CI pipeline (cargo fmt + clippy + tests, frontend type-check + build)

Self-update system (git.tx1138.com):
- scripts/self-update.sh: pull, build, install, restart with rollback
- systemd timer checks daily at 3 AM
- update.check RPC does git-based checks when repo is present
- update.git-apply RPC triggers self-update from UI
- Default update URL changed from GitHub to git.tx1138.com
- Git added to ISO package list for fresh installs

Documentation:
- CHANGELOG v1.3.1 with all changes
- README updated (version, update system section)
- BETA-PROGRESS session #6 logged
- architecture-review.html: 4 issues marked FIXED, 8/12 refactoring done

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:52:26 +00:00

2524 lines
131 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Archipelago — Architecture Review & Learning Guide</title>
<style>
:root {
--bg: #000000;
--glass-card: rgba(0, 0, 0, 0.65);
--glass-dark: rgba(0, 0, 0, 0.35);
--glass-darker: rgba(0, 0, 0, 0.6);
--glass-border: rgba(255, 255, 255, 0.18);
--glass-highlight: rgba(255, 255, 255, 0.22);
--glass-blur: 18px;
--glass-blur-strong: 24px;
--shadow-glass: 0 8px 24px rgba(0, 0, 0, 0.45);
--shadow-glass-inset: inset 0 1px 0 rgba(255, 255, 255, 0.22);
--text: rgba(255, 255, 255, 0.9);
--text-muted: rgba(255, 255, 255, 0.6);
--accent: #fb923c;
--accent-dim: rgba(251, 146, 60, 0.15);
--green: #4ade80;
--green-dim: rgba(74, 222, 128, 0.15);
--red: #ef4444;
--red-dim: rgba(239, 68, 68, 0.12);
--blue: #3b82f6;
--blue-dim: rgba(59, 130, 246, 0.12);
--yellow: #facc15;
--yellow-dim: rgba(250, 204, 21, 0.12);
--purple: #a78bfa;
--purple-dim: rgba(167, 139, 250, 0.12);
--radius: 16px;
--radius-sm: 12px;
--transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Avenir Next', system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.7;
font-size: 16px;
}
/* ─── Navigation ─── */
nav {
position: fixed;
top: 0;
left: 0;
width: 280px;
height: 100vh;
background: var(--glass-card);
backdrop-filter: blur(var(--glass-blur-strong));
-webkit-backdrop-filter: blur(var(--glass-blur-strong));
border-right: 1px solid var(--glass-border);
box-shadow: var(--shadow-glass);
overflow-y: auto;
padding: 24px 0;
z-index: 100;
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.15) transparent;
}
nav .logo {
padding: 0 24px 20px;
margin-bottom: 16px;
}
nav .logo h1 {
font-family: 'Montserrat', 'Avenir Next', sans-serif;
font-size: 18px;
font-weight: 700;
color: var(--accent);
letter-spacing: -0.02em;
}
nav .logo p {
font-size: 12px;
color: var(--text-muted);
margin-top: 4px;
}
nav .nav-section {
padding: 8px 16px 4px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-muted);
}
nav a {
display: block;
padding: 6px 24px;
color: var(--text-muted);
text-decoration: none;
font-size: 13px;
transition: all var(--transition);
border-left: 2px solid transparent;
}
nav a:hover, nav a.active {
color: var(--text);
background: rgba(255, 255, 255, 0.06);
border-left-color: var(--accent);
}
/* ─── Main Content ─── */
main {
margin-left: 280px;
max-width: 900px;
padding: 48px 48px 120px;
}
h2 {
font-family: 'Montserrat', 'Avenir Next', sans-serif;
font-size: 28px;
font-weight: 700;
margin: 64px 0 8px;
padding-top: 24px;
color: var(--text);
letter-spacing: -0.02em;
}
h2:first-of-type { margin-top: 0; }
h3 {
font-family: 'Montserrat', 'Avenir Next', sans-serif;
font-size: 20px;
font-weight: 600;
margin: 40px 0 12px;
color: var(--text);
}
h4 {
font-size: 16px;
font-weight: 600;
margin: 24px 0 8px;
color: var(--accent);
}
p { margin: 8px 0 16px; color: var(--text); }
.subtitle {
font-size: 15px;
color: var(--text-muted);
margin-bottom: 32px;
}
/* ─── Hero ─── */
.hero {
text-align: center;
padding: 48px 0 56px;
margin-bottom: 24px;
}
.hero h1 {
font-family: 'Montserrat', 'Avenir Next', sans-serif;
font-size: 42px;
font-weight: 800;
background: linear-gradient(135deg, var(--accent), #f59e0b);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.03em;
}
.hero .tagline {
font-size: 18px;
color: var(--text-muted);
margin-top: 12px;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.hero .meta {
margin-top: 20px;
display: flex;
gap: 16px;
justify-content: center;
flex-wrap: wrap;
}
.hero .meta span {
font-size: 12px;
padding: 4px 12px;
border-radius: 999px;
background: var(--glass-dark);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
color: var(--text-muted);
}
/* ─── Cards (Glass) ─── */
.card {
background: var(--glass-card);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-glass), var(--shadow-glass-inset);
border-radius: var(--radius);
padding: 24px;
margin: 16px 0;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 16px;
margin: 16px 0;
}
.card-sm {
background: var(--glass-darker);
backdrop-filter: blur(var(--glass-blur-strong));
-webkit-backdrop-filter: blur(var(--glass-blur-strong));
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-glass), var(--shadow-glass-inset);
border-radius: var(--radius);
padding: 16px 20px;
transition: transform var(--transition), box-shadow var(--transition);
}
.card-sm:hover {
transform: translateY(-2px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.25);
}
.card-sm h4 { margin: 0 0 6px; font-size: 14px; }
.card-sm p { font-size: 13px; color: var(--text-muted); margin: 0; }
/* ─── Badges ─── */
.badge {
display: inline-block;
font-size: 11px;
font-weight: 600;
padding: 2px 10px;
border-radius: 999px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.badge-green { background: var(--green-dim); color: var(--green); }
.badge-red { background: var(--red-dim); color: var(--red); }
.badge-yellow { background: var(--yellow-dim); color: var(--yellow); }
.badge-blue { background: var(--blue-dim); color: var(--blue); }
.badge-purple { background: var(--purple-dim); color: var(--purple); }
.badge-accent { background: var(--accent-dim); color: var(--accent); }
/* ─── Tables ─── */
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin: 16px 0;
font-size: 14px;
background: var(--glass-card);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
overflow: hidden;
box-shadow: var(--shadow-glass);
}
th {
text-align: left;
padding: 10px 14px;
background: rgba(0, 0, 0, 0.4);
color: var(--text-muted);
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid var(--glass-border);
}
td {
padding: 10px 14px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
vertical-align: top;
}
tr:last-child td { border-bottom: none; }
tr:hover td { background: rgba(255, 255, 255, 0.04); }
/* ─── Code ─── */
code {
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: 13px;
background: rgba(0, 0, 0, 0.4);
padding: 2px 6px;
border-radius: 4px;
color: var(--accent);
}
pre {
background: var(--glass-card);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
padding: 20px;
overflow-x: auto;
margin: 16px 0;
font-size: 13px;
line-height: 1.6;
box-shadow: var(--shadow-glass);
}
pre code {
background: none;
padding: 0;
color: var(--text);
}
.code-comment { color: var(--text-muted); }
.code-keyword { color: var(--purple); }
.code-string { color: var(--green); }
.code-fn { color: var(--blue); }
/* ─── Diagrams (ASCII art in pre) ─── */
.diagram {
background: var(--glass-card);
backdrop-filter: blur(var(--glass-blur-strong));
-webkit-backdrop-filter: blur(var(--glass-blur-strong));
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-glass), var(--shadow-glass-inset);
border-radius: var(--radius);
padding: 24px;
margin: 20px 0;
overflow-x: auto;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 13px;
line-height: 1.5;
color: var(--text-muted);
}
.diagram .highlight { color: var(--accent); font-weight: 600; }
.diagram .green { color: var(--green); }
.diagram .blue { color: var(--blue); }
.diagram .red { color: var(--red); }
.diagram .purple { color: var(--purple); }
/* ─── Callouts ─── */
.callout {
background: var(--glass-card);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
padding: 16px 20px;
margin: 16px 0;
font-size: 14px;
border-left: 3px solid;
box-shadow: var(--shadow-glass);
}
.callout-info { border-color: var(--blue); }
.callout-warn { border-color: var(--yellow); }
.callout-danger { border-color: var(--red); }
.callout-success { border-color: var(--green); }
.callout-learn { border-color: var(--purple); }
.callout strong { display: block; margin-bottom: 4px; }
/* ─── Score cards ─── */
.score-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin: 20px 0;
}
.score-card {
background: var(--glass-darker);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-glass), var(--shadow-glass-inset);
border-radius: var(--radius);
padding: 16px;
text-align: center;
transition: transform var(--transition);
}
.score-card:hover { transform: translateY(-2px); }
.score-card .score {
font-family: 'Montserrat', 'Avenir Next', sans-serif;
font-size: 32px;
font-weight: 800;
margin: 4px 0;
}
.score-card .label {
font-size: 12px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ─── Lists ─── */
ul, ol {
margin: 8px 0 16px 24px;
color: var(--text);
}
li { margin: 4px 0; font-size: 15px; }
li code { font-size: 12px; }
/* ─── Analogy boxes ─── */
.analogy {
background: var(--glass-card);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid rgba(167, 139, 250, 0.25);
border-radius: var(--radius-sm);
padding: 20px;
margin: 16px 0;
box-shadow: var(--shadow-glass);
}
.analogy::before {
content: 'Real-World Analogy';
display: block;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--purple);
margin-bottom: 8px;
}
.analogy p { font-size: 14px; color: var(--text); margin: 0; }
/* ─── Responsive ─── */
@media (max-width: 900px) {
nav { display: none; }
main { margin-left: 0; padding: 24px 20px 80px; }
.hero h1 { font-size: 28px; }
.card-grid { grid-template-columns: 1fr; }
.score-grid { grid-template-columns: repeat(2, 1fr); }
}
/* ─── Smooth scroll ─── */
html { scroll-behavior: smooth; }
/* ─── Separator ─── */
hr {
border: none;
border-top: 1px solid rgba(255, 255, 255, 0.06);
margin: 40px 0;
}
.flow-step {
display: flex;
gap: 16px;
margin: 12px 0;
align-items: flex-start;
}
.flow-step .num {
flex-shrink: 0;
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--accent-dim);
color: var(--accent);
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
}
.flow-step .content { flex: 1; }
.flow-step .content strong { color: var(--accent); }
.flow-step .content p { margin: 0; font-size: 14px; }
/* ─── Comparison helpers ─── */
.check { color: var(--green); font-weight: 700; }
.cross { color: var(--red); opacity: 0.7; }
.partial { color: var(--yellow); }
td.archy-col { background: var(--accent-dim); }
th.archy-col { color: var(--accent); background: var(--accent-dim); }
.card-sm .item-list { margin: 0; padding-left: 16px; }
.card-sm .item-list li { font-size: 13px; margin: 4px 0; color: var(--text-muted); }
/* ─── Theme toggle ─── */
.theme-toggle {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin: 16px 16px 8px;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
color: var(--text-muted);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all var(--transition);
}
.theme-toggle:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--text);
}
.theme-toggle .icon { font-size: 14px; }
/* ─── Light mode ─── */
[data-theme="light"] {
--bg: #FAFAFA;
--glass-card: rgba(255, 255, 255, 0.7);
--glass-dark: rgba(255, 255, 255, 0.5);
--glass-darker: rgba(255, 255, 255, 0.65);
--glass-border: rgba(0, 0, 0, 0.1);
--glass-highlight: rgba(255, 255, 255, 0.8);
--shadow-glass: 0 8px 24px rgba(0, 0, 0, 0.08);
--shadow-glass-inset: inset 0 1px 0 rgba(255, 255, 255, 0.9);
--text: rgba(0, 0, 0, 0.88);
--text-muted: rgba(0, 0, 0, 0.5);
--accent: #ea7c1f;
--accent-dim: rgba(234, 124, 31, 0.1);
--green: #16a34a;
--green-dim: rgba(22, 163, 74, 0.1);
--red: #dc2626;
--red-dim: rgba(220, 38, 38, 0.08);
--blue: #2563eb;
--blue-dim: rgba(37, 99, 235, 0.08);
--yellow: #ca8a04;
--yellow-dim: rgba(202, 138, 4, 0.08);
--purple: #7c3aed;
--purple-dim: rgba(124, 58, 237, 0.08);
}
[data-theme="light"] body { background: var(--bg); }
[data-theme="light"] nav {
background: rgba(255, 255, 255, 0.8);
border-right-color: rgba(0, 0, 0, 0.08);
box-shadow: 0 0 24px rgba(0, 0, 0, 0.06);
}
[data-theme="light"] nav a:hover,
[data-theme="light"] nav a.active {
background: rgba(0, 0, 0, 0.04);
}
[data-theme="light"] th {
background: rgba(0, 0, 0, 0.04);
color: rgba(0, 0, 0, 0.55);
border-bottom-color: rgba(0, 0, 0, 0.08);
}
[data-theme="light"] td {
border-bottom-color: rgba(0, 0, 0, 0.06);
}
[data-theme="light"] tr:hover td {
background: rgba(0, 0, 0, 0.02);
}
[data-theme="light"] code {
background: rgba(0, 0, 0, 0.06);
color: #c2410c;
}
[data-theme="light"] .hero h1 {
background: linear-gradient(135deg, #c2410c, #ea580c);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
[data-theme="light"] .hero .meta span {
background: rgba(0, 0, 0, 0.04);
border-color: rgba(0, 0, 0, 0.1);
color: rgba(0, 0, 0, 0.55);
}
[data-theme="light"] .analogy {
border-color: rgba(124, 58, 237, 0.2);
}
[data-theme="light"] .theme-toggle {
background: rgba(0, 0, 0, 0.04);
border-color: rgba(0, 0, 0, 0.08);
}
[data-theme="light"] .theme-toggle:hover {
background: rgba(0, 0, 0, 0.08);
}
[data-theme="light"] .diagram { color: rgba(0, 0, 0, 0.5); }
[data-theme="light"] hr { border-top-color: rgba(0, 0, 0, 0.08); }
[data-theme="light"] .score-card .score { filter: none; }
/* ─── Reduce motion ─── */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
</style>
</head>
<body>
<!-- ═══════════════════════ NAVIGATION ═══════════════════════ -->
<nav>
<div class="logo">
<h1>Archipelago</h1>
<p>Architecture Review & Guide</p>
</div>
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle light/dark mode">
<span class="icon" id="theme-icon">&#9790;</span>
<span id="theme-label">Light mode</span>
</button>
<div class="nav-section">Overview</div>
<a href="#what-is-it">What Is Archipelago?</a>
<a href="#big-picture">The Big Picture</a>
<a href="#how-it-runs">How It Runs on a Machine</a>
<div class="nav-section">Architecture</div>
<a href="#layers">The Four Layers</a>
<a href="#rust-backend">Rust Backend</a>
<a href="#vue-frontend">Vue.js Frontend</a>
<a href="#containers">Container System</a>
<a href="#nginx">Nginx Routing</a>
<a href="#data-flow">How Data Flows</a>
<div class="nav-section">Key Concepts</div>
<a href="#rpc">RPC: How Frontend Talks to Backend</a>
<a href="#state">State Management</a>
<a href="#auth">Authentication & Sessions</a>
<a href="#security">Security Model</a>
<a href="#bitcoin">Bitcoin Integration</a>
<a href="#federation">Federation & Multi-Node</a>
<a href="#mesh">Mesh Networking</a>
<div class="nav-section">Build & Deploy</div>
<a href="#deploy-system">Deploy System</a>
<a href="#iso-build">ISO Build Process</a>
<a href="#first-boot">First Boot Sequence</a>
<div class="nav-section">Code Review</div>
<a href="#scores">Quality Scores</a>
<a href="#whats-good">What's Done Well</a>
<a href="#whats-wrong">What Needs Fixing</a>
<a href="#refactor-plan">Refactoring Priorities</a>
<a href="#tech-debt">Technical Debt Map</a>
<div class="nav-section">Learning Path</div>
<a href="#learning">Recommended Study Order</a>
<a href="#glossary">Glossary</a>
</nav>
<!-- ═══════════════════════ MAIN CONTENT ═══════════════════════ -->
<main>
<!-- ─── Hero ─── -->
<div class="hero">
<h1>Archipelago</h1>
<p class="tagline">A complete architecture review and learning guide for the Bitcoin Node OS — explained so anyone can understand it.</p>
<div class="meta">
<span>Rust + Vue 3 + Podman</span>
<span>~45,000 lines of Rust (213 files)</span>
<span>~45,500 lines of TypeScript/Vue (232 files)</span>
<span>~40 shell scripts</span>
<span>v0.1.0-beta</span>
</div>
</div>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="what-is-it">What Is Archipelago?</h2>
<p>Archipelago (nicknamed "Archy") is a <strong>personal server operating system</strong> focused on Bitcoin. You download an ISO file, flash it to a USB drive, install it on any computer, and it gives you:</p>
<ul>
<li>A <strong>full Bitcoin node</strong> — you verify your own transactions, no trust in anyone else</li>
<li>A <strong>Lightning Network node</strong> — fast, cheap Bitcoin payments</li>
<li>A <strong>web dashboard</strong> — manage everything from your phone or laptop browser</li>
<li>An <strong>app marketplace</strong> — install apps like Nextcloud, Jellyfin, Vaultwarden with one click</li>
<li><strong>Privacy by default</strong> — Tor routing, encrypted secrets, no telemetry</li>
</ul>
<div class="analogy">
<p>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? <em>You</em> own and control everything — your data never leaves your machine.</p>
</div>
<p>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.</p>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="big-picture">The Big Picture</h2>
<p>Before diving into code, understand the <strong>four layers</strong> of the system and how they stack:</p>
<div class="diagram">
<span class="highlight">┌──────────────────────────────────────────────────────┐</span>
<span class="highlight">│ YOUR BROWSER │</span>
<span class="highlight">│ (Vue.js Single Page Application) │</span>
<span class="highlight">└──────────────────────┬───────────────────────────────┘</span>
│ HTTP requests (fetch API)
<span class="blue">┌──────────────────────┴───────────────────────────────┐</span>
<span class="blue">│ NGINX │</span>
<span class="blue">│ Reverse proxy — routes traffic to the right place │</span>
<span class="blue">│ /rpc/v1 → backend /app/bitcoin/ → container │</span>
<span class="blue">└──────────────────────┬───────────────────────────────┘</span>
│ Internal HTTP (port 5678)
<span class="green">┌──────────────────────┴───────────────────────────────┐</span>
<span class="green">│ RUST BACKEND │</span>
<span class="green">│ The brain — handles auth, app installs, Bitcoin │</span>
<span class="green">│ RPC, mesh networking, federation, health checks │</span>
<span class="green">└──────────────────────┬───────────────────────────────┘</span>
│ Podman REST API (Unix socket)
<span class="purple">┌──────────────────────┴───────────────────────────────┐</span>
<span class="purple">│ PODMAN CONTAINERS │</span>
<span class="purple">│ Bitcoin Core, LND, Mempool, Nextcloud, etc. │</span>
<span class="purple">│ Each app runs isolated in its own container │</span>
<span class="purple">└──────────────────────────────────────────────────────┘</span>
<span class="red">┌──────────────────────────────────────────────────────┐</span>
<span class="red">│ DEBIAN 12 (Linux OS) │</span>
<span class="red">│ The foundation — systemd, firewall, filesystem │</span>
<span class="red">└──────────────────────────────────────────────────────┘</span>
</div>
<div class="callout callout-learn">
<strong>Key Concept: Separation of Concerns</strong>
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.
</div>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="how-it-runs">How It Runs on a Machine</h2>
<p>When you install Archipelago on a computer and power it on, here's what happens in order:</p>
<div class="flow-step"><div class="num">1</div><div class="content"><p><strong>Linux boots</strong> — Debian 12 starts up, loads drivers, mounts disks</p></div></div>
<div class="flow-step"><div class="num">2</div><div class="content"><p><strong>systemd starts services</strong> — A program called <code>systemd</code> reads <code>archipelago.service</code> and launches the Rust backend</p></div></div>
<div class="flow-step"><div class="num">3</div><div class="content"><p><strong>Rust backend initializes</strong> — Loads config, creates/loads encryption keys, starts the HTTP server on port 5678</p></div></div>
<div class="flow-step"><div class="num">4</div><div class="content"><p><strong>Health monitor starts</strong> — Checks which containers are running, restarts crashed ones, reports readiness</p></div></div>
<div class="flow-step"><div class="num">5</div><div class="content"><p><strong>Nginx starts</strong> — Listens on port 80 (HTTP) and routes all incoming traffic</p></div></div>
<div class="flow-step"><div class="num">6</div><div class="content"><p><strong>Containers start</strong> — Bitcoin, LND, and other apps start in priority order (Bitcoin first, then things that depend on it)</p></div></div>
<div class="flow-step"><div class="num">7</div><div class="content"><p><strong>Ready!</strong> — You open a browser, go to your server's IP address, and see the dashboard</p></div></div>
<div class="analogy">
<p>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).</p>
</div>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="layers">The Four Layers — Detailed</h2>
<h3 id="rust-backend">Layer 1: The Rust Backend (The Brain)</h3>
<p>This is the most important piece. It's written in <strong>Rust</strong> — a programming language known for speed and safety. The backend is the "brain" that controls everything.</p>
<div class="callout callout-learn">
<strong>Why Rust?</strong>
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.
</div>
<h4>How the code is organized</h4>
<p>The Rust code lives in <code>core/</code> and is split into 5 workspace crates (consolidated from 9 during recent refactoring):</p>
<table>
<tr><th>Crate</th><th>What It Does</th><th>Lines</th><th>Analogy</th></tr>
<tr>
<td><code>archipelago</code></td>
<td>The main binary. API endpoints, auth, identity, federation, mesh networking, monitoring, health checks</td>
<td>~42,000</td>
<td>The restaurant manager — coordinates everything</td>
</tr>
<tr>
<td><code>container</code></td>
<td>PodmanClient (REST API socket), manifest parser, dependency resolver, health monitor, Bitcoin simulator</td>
<td>~2,060</td>
<td>The kitchen manager — controls cook stations</td>
</tr>
<tr>
<td><code>security</code></td>
<td>Encrypted secrets (Argon2 + ChaCha20-Poly1305), AppArmor profiles, Cosign image verification</td>
<td>~743</td>
<td>The security guard — locks doors, checks IDs</td>
</tr>
<tr>
<td><code>parmanode</code></td>
<td>Compatibility layer for migrating from an older project</td>
<td>~234</td>
<td>A translation book — speaks the old language</td>
</tr>
<tr>
<td><code>performance</code></td>
<td>CPU, memory, and disk resource management</td>
<td>~92</td>
<td>The meter reader — watches resource gauges</td>
</tr>
</table>
<h4>Key modules you should know</h4>
<p>The recent refactoring split monolithic files into focused module directories. Each directory has a <code>mod.rs</code> entry point and focused sub-files:</p>
<table>
<tr><th>Module</th><th>What It Does</th><th>Lines</th><th>Structure</th></tr>
<tr><td><code>main.rs</code></td><td>Entry point — starts server, registers signal handlers</td><td>~180</td><td>Single file</td></tr>
<tr><td><code>server.rs</code></td><td>Wires HTTP server, connects all components</td><td>~506</td><td>Single file</td></tr>
<tr><td><code>api/handler/</code></td><td>HTTP request routing, CORS, WebSocket upgrade, auth</td><td>~896</td><td>mod.rs + content, dwn, node_message, proxy, websocket</td></tr>
<tr><td><code>api/rpc/</code></td><td>RPC dispatch, 29 endpoint modules + 8 subdirectories</td><td>~20,000</td><td>dispatcher.rs routes to focused handlers</td></tr>
<tr><td><code>api/rpc/package/</code></td><td>App lifecycle — install, config, runtime, progress, deps</td><td>~2,248</td><td>config.rs, install.rs, lifecycle.rs, runtime.rs, stacks.rs, dependencies.rs, progress.rs</td></tr>
<tr><td><code>mesh/</code></td><td>LoRa mesh networking — protocol, crypto, serial, relay</td><td>~6,000</td><td>13 files + listener/ subdirectory (6 files)</td></tr>
<tr><td><code>federation/</code></td><td>Multi-node federation — invites, sync, storage</td><td>~782</td><td>invites.rs, storage.rs, sync.rs, types.rs</td></tr>
<tr><td><code>credentials/</code></td><td>W3C Verifiable Credentials — CRUD, presentation</td><td>~803</td><td>operations.rs, presentation.rs, store.rs, types.rs</td></tr>
<tr><td><code>monitoring/</code></td><td>Metrics collection, alerts, beta telemetry</td><td>~1,380</td><td>collector.rs, store.rs, alerts.rs, telemetry.rs, notifications.rs, types.rs</td></tr>
<tr><td><code>session.rs</code></td><td>Session management, remember-me, cookie handling</td><td>~622</td><td>Single file</td></tr>
<tr><td><code>health_monitor.rs</code></td><td>Container health, auto-restart, system alerts</td><td>~731</td><td>Single file</td></tr>
<tr><td><code>rate_limit.rs</code></td><td>Per-IP login + endpoint rate limiting</td><td>~191</td><td>Single file (new)</td></tr>
</table>
<h4>How the backend handles a request</h4>
<div class="diagram">
Browser sends: POST /rpc/v1
Body: { <span class="string">"method"</span>: <span class="string">"package.install"</span>, <span class="string">"params"</span>: { <span class="string">"id"</span>: <span class="string">"bitcoin-knots"</span> } }
<span class="highlight">Step 1:</span> Nginx receives it on port 80, forwards to port 5678
<span class="highlight">Step 2:</span> Rust HTTP server (Hyper) receives the raw bytes
<span class="highlight">Step 3:</span> <span class="blue">handler/mod.rs</span> parses the JSON, extracts the method name
<span class="highlight">Step 4:</span> <span class="blue">rpc/mod.rs</span> checks the CSRF token (security check)
<span class="highlight">Step 5:</span> <span class="blue">rpc/mod.rs</span> checks the session cookie (are you logged in?)
<span class="highlight">Step 6:</span> <span class="blue">dispatcher.rs</span> routes to <span class="green">package/install.rs</span> based on method name
<span class="highlight">Step 7:</span> <span class="green">package/install.rs</span> validates the app ID
<span class="highlight">Step 8:</span> <span class="green">package/dependencies.rs</span> checks dependency chain
<span class="highlight">Step 9:</span> <span class="green">PodmanClient</span> pulls image + creates container via REST API socket
<span class="highlight">Step 10:</span> Response sent back: { <span class="string">"result"</span>: { <span class="string">"state"</span>: <span class="string">"installing"</span> } }
</div>
<hr>
<h3 id="rust-deep-dive">Rust Backend Deep Dive — Should We Use Custom Code?</h3>
<div class="callout callout-learn">
<strong>The short answer:</strong> 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.
</div>
<h4>Why not use an existing solution?</h4>
<p>Projects like Umbrel use a <strong>Node.js + Docker Compose</strong> backend. Start9 uses <strong>Rust</strong> (like us). RaspiBlitz uses <strong>bash scripts</strong>. Here's why custom Rust wins:</p>
<table>
<tr><th>Approach</th><th>Pros</th><th>Cons</th></tr>
<tr>
<td><code>Node.js</code> (Umbrel-style)</td>
<td>Fast to develop, large ecosystem</td>
<td>Memory-unsafe (crypto bugs), GC pauses, runtime dependency, <code>node_modules</code> supply chain risk</td>
</tr>
<tr>
<td><code>Bash scripts</code> (RaspiBlitz-style)</td>
<td>Simple, no compilation</td>
<td>Unmaintainable at scale, no type safety, fragile error handling, injection risks</td>
</tr>
<tr>
<td><code>Go</code></td>
<td>Single binary, good concurrency</td>
<td>No zero-cost abstractions, GC pauses, weaker type system than Rust</td>
</tr>
<tr>
<td><span class="badge badge-green">Rust</span> (our choice)</td>
<td>Single binary, zero-cost abstractions, memory safety without GC, excellent crypto ecosystem, <code>zeroize</code> for key material</td>
<td>Steeper learning curve, slower compile times</td>
</tr>
</table>
<h4>RPC Endpoint Architecture (Refactored)</h4>
<p>Every action the frontend takes goes through <code>POST /rpc/v1</code> as a JSON-RPC call. The RPC layer was recently refactored from monolithic files into <strong>29 standalone modules + 8 domain subdirectories</strong>, totaling ~20,000 lines. Requests flow through <code>dispatcher.rs</code> (395 LOC) which routes to the appropriate handler:</p>
<table>
<tr><th>Category</th><th>Module</th><th>Lines</th><th>Key Methods</th></tr>
<tr>
<td rowspan="3"><span class="badge badge-accent">App Lifecycle</span></td>
<td><code>package/</code> (7 files)</td><td>2,248</td>
<td><code>package.install</code>, <code>package.start</code>, <code>package.stop</code>, <code>package.uninstall</code>, <code>package.stacks</code></td>
</tr>
<tr>
<td><code>container.rs</code></td><td>413</td>
<td><code>container.logs</code>, <code>container.inspect</code></td>
</tr>
<tr>
<td><code>marketplace.rs</code></td><td>225</td>
<td><code>marketplace.list</code>, <code>marketplace.search</code></td>
</tr>
<tr>
<td rowspan="4"><span class="badge badge-green">Auth &amp; Security</span></td>
<td><code>auth.rs</code></td><td>102</td>
<td><code>auth.login</code>, <code>auth.login.totp</code>, <code>auth.logout</code></td>
</tr>
<tr>
<td><code>totp.rs</code></td><td>295</td>
<td><code>totp.enable</code>, <code>totp.verify</code>, <code>totp.disable</code></td>
</tr>
<tr>
<td><code>credentials.rs</code></td><td>274</td>
<td><code>credentials.issue</code>, <code>credentials.verify</code> (W3C Verifiable Credentials)</td>
</tr>
<tr>
<td><code>security.rs</code></td><td>66</td>
<td>AppArmor policy management</td>
</tr>
<tr>
<td rowspan="3"><span class="badge badge-accent">Bitcoin</span></td>
<td><code>bitcoin.rs</code></td><td>96</td>
<td><code>bitcoin.getblockchaininfo</code>, <code>bitcoin.getpeerinfo</code> — RPC passthrough</td>
</tr>
<tr>
<td><code>lnd/</code> (5 files)</td><td>1,092</td>
<td><code>lnd.getinfo</code>, <code>lnd.walletbalance</code>, <code>lnd.channels</code>, <code>lnd.payments</code></td>
</tr>
<tr>
<td><code>wallet.rs</code></td><td>108</td>
<td><code>wallet.balance</code>, <code>wallet.transactions</code></td>
</tr>
<tr>
<td rowspan="3"><span class="badge badge-purple">System</span></td>
<td><code>system/</code> (2 files)</td><td>777</td>
<td><code>system.stats</code>, <code>system.reboot</code>, <code>system.factory-reset</code></td>
</tr>
<tr>
<td><code>monitoring.rs</code></td><td>216</td>
<td><code>monitoring.containers</code>, <code>monitoring.resources</code></td>
</tr>
<tr>
<td><code>update.rs</code></td><td>108</td>
<td><code>update.check</code>, <code>update.apply</code></td>
</tr>
<tr>
<td rowspan="3"><span class="badge badge-blue">Identity &amp; Federation</span></td>
<td><code>identity/</code> (2 files)</td><td>778</td>
<td><code>identity.create</code>, <code>identity.export</code>, <code>identity.did</code></td>
</tr>
<tr>
<td><code>federation/</code> (2 files)</td><td>732</td>
<td><code>federation.list-nodes</code>, <code>federation.pair</code>, <code>federation.sync</code></td>
</tr>
<tr>
<td><code>mesh/</code> (6 files)</td><td>885</td>
<td><code>mesh.status</code>, <code>mesh.send</code>, <code>mesh.peers</code>, <code>mesh.bitcoin-ops</code></td>
</tr>
<tr>
<td rowspan="2"><span class="badge badge-yellow">Network</span></td>
<td><code>tor/</code> (2 files)</td><td>769</td>
<td><code>tor.status</code>, <code>tor.create-service</code>, <code>tor.get-address</code></td>
</tr>
<tr>
<td><code>vpn.rs</code></td><td>229</td>
<td><code>vpn.status</code>, <code>vpn.configure</code></td>
</tr>
<tr>
<td rowspan="5"><span class="badge badge-yellow">Other</span></td>
<td><code>analytics.rs</code></td><td>438</td>
<td>Event analytics, usage tracking</td>
</tr>
<tr>
<td><code>interfaces.rs</code></td><td>442</td>
<td>Network interface management</td>
</tr>
<tr>
<td><code>backup_rpc.rs</code></td><td>394</td>
<td><code>backup.create</code>, <code>backup.restore</code></td>
</tr>
<tr>
<td><code>content.rs</code></td><td>352</td>
<td>Peer content distribution</td>
</tr>
<tr>
<td><code>transport.rs</code></td><td>157</td>
<td>Transport layer abstraction</td>
</tr>
</table>
<div class="callout callout-success">
<strong>Refactoring win:</strong> The old monolithic <code>package.rs</code> (1,795 lines) was split into 7 focused files under <code>package/</code>. Similarly, <code>federation.rs</code>, <code>identity.rs</code>, <code>mesh.rs</code>, <code>system.rs</code>, and <code>tor.rs</code> were each extracted into their own subdirectories with <code>handlers.rs</code> + <code>mod.rs</code> separation. The API handler layer (<code>handler.rs</code>) was split into 6 focused files under <code>api/handler/</code>.
</div>
<h4>Container Orchestration — How Podman Is Controlled</h4>
<p>The backend talks to <strong>rootless Podman</strong> via its Unix socket REST API (not CLI). This is faster, more reliable, and avoids shell injection risks.</p>
<div class="diagram">
<span class="highlight">PodmanClient</span> connects to:
/run/user/1000/podman/podman.sock (API v4.0.0)
<span class="highlight">Install flow:</span>
1. <span class="blue">package.rs</span> validates app ID + checks dependencies
2. <span class="blue">DependencyResolver</span> topological sort → install order
3. <span class="green">PodmanClient</span>.pull_image() → downloads container image
4. <span class="green">PodmanClient</span>.create_container() → sets ports, volumes, caps, memory limits
5. <span class="green">PodmanClient</span>.start_container()
6. <span class="blue">HealthMonitor</span> begins watching (60s intervals)
<span class="highlight">Crash recovery:</span>
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)
</div>
<h4>Security Architecture</h4>
<table>
<tr><th>Layer</th><th>Mechanism</th><th>Implementation</th></tr>
<tr>
<td>Secrets at rest</td>
<td>AES-256-GCM encryption</td>
<td><code>core/security/secrets_manager.rs</code> — encrypts to <code>/var/lib/archipelago/secrets/</code></td>
</tr>
<tr>
<td>Node identity</td>
<td>Ed25519 keypair</td>
<td>Generated on first boot, stored at <code>/var/lib/archipelago/identity/</code></td>
</tr>
<tr>
<td>Image verification</td>
<td>Cosign signatures</td>
<td><code>core/security/image_verifier.rs</code> — verifies container image provenance</td>
</tr>
<tr>
<td>Sessions</td>
<td>32-byte random tokens</td>
<td><code>OsRng</code>, 24h TTL, persisted to <code>sessions.json</code>, zeroized on drop</td>
</tr>
<tr>
<td>2FA</td>
<td>TOTP (RFC 6238)</td>
<td>5 attempt lockout, 5min pending session TTL, token rotation after verification</td>
</tr>
<tr>
<td>Rate limiting</td>
<td>Per-IP + per-endpoint</td>
<td>Login endpoints rate-limited, IP extracted from <code>X-Real-IP</code> (loopback only)</td>
</tr>
<tr>
<td>RBAC</td>
<td>Explicit method allowlists</td>
<td>No prefix matching — each role lists exact permitted methods</td>
</tr>
<tr>
<td>Key material</td>
<td><code>zeroize::Zeroizing</code></td>
<td>All crypto keys zeroed from memory after use</td>
</tr>
</table>
<h4>WebSocket Real-Time Sync</h4>
<p>The frontend connects to <code>/ws/db</code> and receives the full <strong>DataModel</strong> on connect, then incremental updates as state changes. This is how the UI shows live container status, sync progress, and notifications without polling.</p>
<div class="diagram">
<span class="highlight">DataModel</span> (broadcast to all WebSocket clients):
{
<span class="green">server_info</span>: { node_id, name, tor_address, lan_ip, version }
<span class="green">package_data</span>: {
"bitcoin-knots": { state: "running", health: "healthy", ... }
"lnd": { state: "running", health: "healthy", ... }
"mempool": { state: "stopped", health: null, ... }
}
<span class="green">peer_health</span>: { "did:key:z6Mk...": true }
<span class="green">notifications</span>: [ { type: "warning", message: "Disk 85% full" } ]
}
</div>
<h4>What's custom vs. what could be replaced?</h4>
<table>
<tr><th>Component</th><th>Custom?</th><th>Could it be replaced?</th></tr>
<tr>
<td>HTTP server</td>
<td>Uses <code>hyper</code> (standard Rust HTTP)</td>
<td>Could use <code>axum</code> or <code>actix-web</code> for ergonomics, but hyper is fine</td>
</tr>
<tr>
<td>RPC routing</td>
<td><span class="badge badge-accent">Custom</span> — hand-rolled JSON-RPC dispatcher</td>
<td>Could use <code>jsonrpsee</code> or generate from OpenAPI, but the current router is simple and works</td>
</tr>
<tr>
<td>Container orchestration</td>
<td><span class="badge badge-accent">Custom</span> — PodmanClient + health monitor</td>
<td>No off-the-shelf alternative for rootless Podman orchestration with Bitcoin-specific dependency ordering</td>
</tr>
<tr>
<td>Secrets management</td>
<td><span class="badge badge-accent">Custom</span> — AES-256-GCM with Zeroize</td>
<td>Could use <code>age</code> or <code>sops</code>, but inline encryption is simpler for container secrets</td>
</tr>
<tr>
<td>Federation/Mesh</td>
<td><span class="badge badge-accent">Custom</span> — Ed25519 signed messages, Nostr discovery, DWN</td>
<td>No existing solution does Bitcoin node federation + mesh radio. This is novel.</td>
</tr>
<tr>
<td>Auth/Sessions</td>
<td><span class="badge badge-accent">Custom</span></td>
<td>Could use a library, but the session model is simple (32-byte tokens + file persistence)</td>
</tr>
<tr>
<td>Bitcoin/LND RPC</td>
<td><span class="badge badge-accent">Custom</span> passthrough</td>
<td>Must be custom — proxies authenticated calls to local Bitcoin/LND with macaroon management</td>
</tr>
</table>
<div class="callout callout-learn">
<strong>Bottom line:</strong> 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.
</div>
<hr>
<h3 id="vue-frontend">Layer 2: The Vue.js Frontend (The Face)</h3>
<p>The frontend is what you see in the browser. It's built with <strong>Vue 3</strong> — a JavaScript framework for building interactive web pages — and <strong>TypeScript</strong> — JavaScript with type safety.</p>
<div class="callout callout-learn">
<strong>What is a Single Page Application (SPA)?</strong>
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.
</div>
<h4>Frontend file structure (refactored)</h4>
<p>The frontend was heavily refactored — large "god components" were split into focused sub-views, and the god store was decomposed into dedicated stores:</p>
<pre><code>neode-ui/src/
├── <span class="code-keyword">api/</span> <span class="code-comment">← Backend communication (4 files + 1 service)</span>
│ ├── rpc-client.ts <span class="code-comment">← RPC client (18.8 KB) — ~70 methods, retry, CSRF</span>
│ ├── websocket.ts <span class="code-comment">← WebSocket (16.3 KB) — JSON patch (RFC 6902)</span>
│ ├── container-client.ts <span class="code-comment">← Container API helpers</span>
│ ├── filebrowser-client.ts <span class="code-comment">← FileBrowser API</span>
│ └── services/contextBroker.ts <span class="code-comment">← Context management (21.9 KB)</span>
├── <span class="code-keyword">views/</span> <span class="code-comment">← 37 top-level + 47 sub-views in 14 subdirectories</span>
│ ├── Dashboard.vue <span class="code-comment">← Main layout with sidebar</span>
│ ├── <span class="code-keyword">dashboard/</span> <span class="code-comment">← Sidebar, MobileNav, ConnectionBanner (6 files)</span>
│ ├── <span class="code-keyword">apps/</span> <span class="code-comment">← AppCard, UninstallModal, config (5 files)</span>
│ ├── <span class="code-keyword">appDetails/</span> <span class="code-comment">← HeroSection, ContentSection, Sidebar (4 files)</span>
│ ├── <span class="code-keyword">appSession/</span> <span class="code-comment">← Frame, Header, NostrBridge, AppIdentity (5 files)</span>
│ ├── <span class="code-keyword">discover/</span> <span class="code-comment">← Hero, AppGrid, FeaturedApps, FilterModal (6 files)</span>
│ ├── <span class="code-keyword">federation/</span> <span class="code-comment">← Header, NodeList, JoinModal, RotateDid (8 files)</span>
│ ├── <span class="code-keyword">fleet/</span> <span class="code-comment">← NodeGrid, ContainerMatrix, Alerts, Overview (6 files)</span>
│ ├── <span class="code-keyword">mesh/</span> <span class="code-comment">← BitcoinPanel, DeadmanPanel, styles (3 files)</span>
│ ├── <span class="code-keyword">settings/</span> <span class="code-comment">← 13 focused sections (Account, 2FA, Backup, etc.)</span>
│ ├── <span class="code-keyword">web5/</span> <span class="code-comment">← 14 sub-views (DID, Wallet, Nostr, DWN, etc.)</span>
│ ├── <span class="code-keyword">marketplace/</span> <span class="code-comment">← AppCard, FilterModal, marketplaceData (3 files)</span>
│ ├── <span class="code-keyword">server/</span> <span class="code-comment">← QuickActions, Modals, TorServices (3 files)</span>
│ └── <span class="code-keyword">home/</span> <span class="code-comment">← SystemCard, WalletCard (2 files)</span>
├── <span class="code-keyword">components/</span> <span class="code-comment">← 31 reusable components + 6 in subdirectories</span>
│ ├── BootScreen.vue, SplashScreen.vue, SpotlightSearch.vue
│ ├── BaseModal.vue, ToastStack.vue, SkeletonCard.vue, EmptyState.vue
│ ├── MeshMap.vue, LineChart.vue, AnimatedLogo.vue
│ ├── <span class="code-keyword">cloud/</span> <span class="code-comment">← FileCard, FileGrid, ShareModal (5 files)</span>
│ └── <span class="code-keyword">federation/</span> <span class="code-comment">← NetworkMap.vue</span>
├── <span class="code-keyword">stores/</span> <span class="code-comment">← 18 Pinia stores (decomposed from god store)</span>
│ ├── app.ts <span class="code-comment">← Core app state (slimmed down)</span>
│ ├── auth.ts <span class="code-comment">← Login, logout, TOTP, sessions (NEW)</span>
│ ├── server.ts <span class="code-comment">← Server state, package actions (NEW)</span>
│ ├── sync.ts <span class="code-comment">← WebSocket, real-time data, JSON patch (NEW)</span>
│ ├── container.ts <span class="code-comment">← Container states &amp; lifecycle</span>
│ ├── mesh.ts <span class="code-comment">← Mesh networking (14 KB — largest store)</span>
│ ├── appLauncher.ts <span class="code-comment">← App iframe management (11 KB)</span>
│ └── ... 11 more focused stores
├── <span class="code-keyword">composables/</span> <span class="code-comment">← 11 composables + 10 test files</span>
│ ├── useToast.ts, useControllerNav.ts (16.9 KB)
│ ├── useLoginSounds.ts, useNavSounds.ts, useAudioPlayer.ts
│ └── useOnboarding.ts, useModalKeyboard.ts, useMobileBackButton.ts
├── <span class="code-keyword">types/</span> <span class="code-comment">← TypeScript type definitions (3 files)</span>
│ ├── api.ts <span class="code-comment">← RPC methods, responses, DataModel, PatchOperation</span>
│ └── aiui-protocol.ts <span class="code-comment">← AIUI communication protocol</span>
├── <span class="code-keyword">router/</span> <span class="code-comment">← Route definitions (9.5 KB) — lazy-loaded + nav guards</span>
└── style.css <span class="code-comment">← Global glassmorphism theme + Tailwind utilities</span></code></pre>
<h4>How a Vue component works</h4>
<p>Every <code>.vue</code> file has three sections:</p>
<pre><code><span class="code-comment">&lt;!-- 1. THE LOGIC (TypeScript) --&gt;</span>
<span class="code-keyword">&lt;script setup lang="ts"&gt;</span>
<span class="code-keyword">import</span> { ref, onMounted } <span class="code-keyword">from</span> <span class="code-string">'vue'</span>
<span class="code-keyword">import</span> { rpcClient } <span class="code-keyword">from</span> <span class="code-string">'@/api/rpc-client'</span>
<span class="code-comment">// "ref" is a reactive variable — when it changes, the UI updates automatically</span>
<span class="code-keyword">const</span> apps = <span class="code-fn">ref</span>([])
<span class="code-keyword">const</span> loading = <span class="code-fn">ref</span>(<span class="code-keyword">true</span>)
<span class="code-comment">// "onMounted" runs when the component first appears on screen</span>
<span class="code-fn">onMounted</span>(<span class="code-keyword">async</span> () => {
apps.value = <span class="code-keyword">await</span> rpcClient.<span class="code-fn">getMarketplace</span>()
loading.value = <span class="code-keyword">false</span>
})
<span class="code-keyword">&lt;/script&gt;</span>
<span class="code-comment">&lt;!-- 2. THE TEMPLATE (HTML with Vue directives) --&gt;</span>
<span class="code-keyword">&lt;template&gt;</span>
&lt;div v-if="loading"&gt;Loading...&lt;/div&gt;
&lt;div v-else v-for="app in apps" class="glass-card"&gt;
{{ app.name }}
&lt;/div&gt;
<span class="code-keyword">&lt;/template&gt;</span>
<span class="code-comment">&lt;!-- 3. THE STYLES (CSS, scoped to this component) --&gt;</span>
<span class="code-keyword">&lt;style scoped&gt;</span>
<span class="code-comment">/* Styles here only affect THIS component */</span>
<span class="code-keyword">&lt;/style&gt;</span></code></pre>
<div class="analogy">
<p>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 <code>&lt;Dashboard&gt;</code> component contains <code>&lt;Sidebar&gt;</code>, which contains <code>&lt;NavItem&gt;</code> components — just like nesting LEGO bricks.</p>
</div>
<hr>
<h3 id="containers">Layer 3: The Container System (The Apps)</h3>
<p>Containers are how Archipelago runs apps like Bitcoin Core, Lightning, Nextcloud, etc. Each app runs in its own isolated "box" called a container.</p>
<div class="callout callout-learn">
<strong>What is a Container?</strong>
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.
</div>
<p>Archipelago uses <strong>rootless Podman</strong> instead of Docker. Podman runs entirely without root privileges under the <code>archipelago</code> user (UID 1000) — no background daemon, no root access needed. The backend communicates with Podman via its REST API socket, not the CLI.</p>
<h4>Container security rules</h4>
<p>Every container in Archipelago follows strict security rules:</p>
<table>
<tr><th>Rule</th><th>What It Means</th><th>Why</th></tr>
<tr>
<td><code>--cap-drop=ALL</code></td>
<td>Remove all Linux capabilities (super-powers)</td>
<td>A hacked container can't do anything dangerous</td>
</tr>
<tr>
<td><code>--cap-add=CHOWN</code></td>
<td>Give back only the specific powers needed</td>
<td>Minimum privilege — only what's necessary</td>
</tr>
<tr>
<td><code>readonly_root: true</code></td>
<td>Container can't modify its own program files</td>
<td>Prevents malware from modifying the app</td>
</tr>
<tr>
<td><code>--user 1001:1001</code></td>
<td>Run as non-root user</td>
<td>Even if exploited, can't access system files</td>
</tr>
<tr>
<td><code>no-new-privileges</code></td>
<td>Can't escalate to higher permissions</td>
<td>Prevents privilege escalation attacks</td>
</tr>
</table>
<h4>Container startup order (tiers)</h4>
<div class="diagram">
<span class="green">Tier 1: Foundation</span> (start first, other apps depend on these)
├── Bitcoin Core/Knots <span class="code-comment">← The blockchain</span>
├── MySQL/PostgreSQL <span class="code-comment">← Databases</span>
└── Redis <span class="code-comment">← Cache</span>
<span class="blue">Tier 2: Core Services</span> (need Tier 1 to be running)
├── LND (Lightning) <span class="code-comment">← Needs Bitcoin</span>
├── ElectrumX <span class="code-comment">← Needs Bitcoin</span>
├── Mempool <span class="code-comment">← Needs Bitcoin + ElectrumX</span>
└── BTCPay Server <span class="code-comment">← Needs Bitcoin + LND</span>
<span class="purple">Tier 3: Applications</span> (independent or need Tier 2)
├── Nextcloud, Jellyfin <span class="code-comment">← File storage, media</span>
├── Vaultwarden <span class="code-comment">← Password manager</span>
├── Home Assistant <span class="code-comment">← Smart home</span>
└── Grafana <span class="code-comment">← Monitoring dashboards</span>
</div>
<hr>
<h3 id="nginx">Layer 4: Nginx (The Traffic Cop)</h3>
<p><strong>Nginx</strong> (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.</p>
<div class="analogy">
<p>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.</p>
</div>
<!-- ─── Comparison: Why Nginx? ─── -->
<h4>Why Nginx? Comparing Reverse Proxies</h4>
<p>Every node OS needs a reverse proxy to route traffic. Here's how the major projects differ:</p>
<div class="card-grid">
<div class="card-sm" style="border-color: var(--accent); border-width: 2px;">
<h4>Nginx <span class="badge badge-accent">Archipelago</span></h4>
<p><span class="check">&#10003;</span> Battle-tested (30+ years)<br>
<span class="check">&#10003;</span> Sub-millisecond routing<br>
<span class="check">&#10003;</span> Fine-grained rate limiting<br>
<span class="check">&#10003;</span> sub_filter HTML rewriting<br>
<span class="check">&#10003;</span> Full CSP / HSTS control<br>
<span class="partial">~</span> Manual config (1,100 lines)<br>
<span class="cross">&#10007;</span> No auto-TLS (manual certs)</p>
</div>
<div class="card-sm">
<h4>Caddy <span class="badge badge-blue">Umbrel</span></h4>
<p><span class="check">&#10003;</span> Automatic HTTPS / Let's Encrypt<br>
<span class="check">&#10003;</span> Simple Caddyfile syntax<br>
<span class="check">&#10003;</span> Built-in HTTP/3 support<br>
<span class="cross">&#10007;</span> No sub_filter (needs plugins)<br>
<span class="cross">&#10007;</span> Higher memory footprint<br>
<span class="cross">&#10007;</span> Less granular rate limiting<br>
<span class="partial">~</span> Newer, smaller ecosystem</p>
</div>
<div class="card-sm">
<h4>Tor-only <span class="badge badge-purple">StartOS</span></h4>
<p><span class="check">&#10003;</span> Maximum privacy (no clearnet)<br>
<span class="check">&#10003;</span> No port forwarding needed<br>
<span class="check">&#10003;</span> Built-in NAT traversal<br>
<span class="cross">&#10007;</span> Slow (500ms3s latency)<br>
<span class="cross">&#10007;</span> No LAN access without config<br>
<span class="cross">&#10007;</span> Requires .onion browser support<br>
<span class="cross">&#10007;</span> No WebSocket over Tor (flaky)</p>
</div>
<div class="card-sm">
<h4>NixOS Module <span class="badge badge-green">Nix-Bitcoin</span></h4>
<p><span class="check">&#10003;</span> Declarative, reproducible<br>
<span class="check">&#10003;</span> Atomic rollbacks<br>
<span class="check">&#10003;</span> Any proxy (Nginx/Caddy/HAProxy)<br>
<span class="partial">~</span> Steep learning curve (Nix lang)<br>
<span class="cross">&#10007;</span> No web UI (CLI only)<br>
<span class="cross">&#10007;</span> Not beginner-friendly<br>
<span class="cross">&#10007;</span> Long rebuild times</p>
</div>
</div>
<div class="callout callout-info">
<strong>Archipelago's choice:</strong> 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.
</div>
<!-- ─── Full Comparison Table ─── -->
<h4>Head-to-Head: Architecture Decisions</h4>
<table>
<tr>
<th>Feature</th>
<th class="archy-col">Archipelago</th>
<th>Umbrel</th>
<th>StartOS</th>
<th>Nix-Bitcoin</th>
<th>RaspiBlitz</th>
</tr>
<tr>
<td>Reverse Proxy</td>
<td class="archy-col"><strong>Nginx</strong></td>
<td>Caddy</td>
<td>Tor hidden svc</td>
<td>Nginx (Nix module)</td>
<td>Nginx</td>
</tr>
<tr>
<td>Backend</td>
<td class="archy-col"><strong>Rust</strong></td>
<td>Node.js + Go</td>
<td>Rust (startos)</td>
<td>Shell/Nix</td>
<td>Shell scripts</td>
</tr>
<tr>
<td>Containers</td>
<td class="archy-col"><strong>Rootless Podman</strong></td>
<td>Docker (root)</td>
<td>Docker (root)</td>
<td>None (native pkgs)</td>
<td>Docker (root)</td>
</tr>
<tr>
<td>TLS/HTTPS</td>
<td class="archy-col"><strong>Self-signed + HSTS</strong></td>
<td>Auto (Let's Encrypt)</td>
<td>Tor-only (no TLS)</td>
<td>Let's Encrypt</td>
<td>Self-signed</td>
</tr>
<tr>
<td>Rate Limiting</td>
<td class="archy-col"><strong>Dual-zone (RPC 20r/s + Auth 3r/s)</strong></td>
<td>None</td>
<td>None</td>
<td>Optional (manual)</td>
<td>None</td>
</tr>
<tr>
<td>Security Headers</td>
<td class="archy-col"><strong>Full CSP + HSTS + Permissions</strong></td>
<td>Basic</td>
<td>N/A (Tor)</td>
<td>Configurable</td>
<td>Minimal</td>
</tr>
<tr>
<td>App Isolation</td>
<td class="archy-col"><strong>Cap-drop, readonly root, non-root UID</strong></td>
<td>Docker defaults</td>
<td>Docker + sandboxing</td>
<td>systemd sandboxing</td>
<td>Docker defaults</td>
</tr>
<tr>
<td>LAN + Remote</td>
<td class="archy-col"><strong>LAN + Tailscale + Tor</strong></td>
<td>LAN + Tor + Tailscale</td>
<td>Tor-only (LAN optional)</td>
<td>LAN + WireGuard</td>
<td>LAN + Tor</td>
</tr>
<tr>
<td>WebSocket</td>
<td class="archy-col"><strong>Native (24h timeout)</strong></td>
<td>Polling + WS</td>
<td>SSE over Tor</td>
<td>N/A</td>
<td>Polling</td>
</tr>
<tr>
<td>App UI Injection</td>
<td class="archy-col"><strong>sub_filter (Nostr NIP-07)</strong></td>
<td>None</td>
<td>None</td>
<td>N/A</td>
<td>None</td>
</tr>
</table>
<!-- ─── Routing Map ─── -->
<h4>How Nginx Routes Traffic</h4>
<p>The config defines 30+ <code>location</code> blocks across HTTP (port 80) and HTTPS (port 443). Here are the major routing categories:</p>
<div class="card">
<h4>Backend & API Routes</h4>
<table>
<tr><th>URL Pattern</th><th>Backend</th><th>Rate Limit</th><th>Timeout</th><th>Purpose</th></tr>
<tr><td><code>/rpc/</code></td><td>:5678</td><td>20r/s (burst 40)</td><td>600s</td><td>All RPC API calls (1MB body limit)</td></tr>
<tr><td><code>/ws</code></td><td>:5678</td><td></td><td>86,400s (24h)</td><td>WebSocket — real-time state updates</td></tr>
<tr><td><code>/health</code></td><td>:5678</td><td></td><td>default</td><td>Health check (no auth)</td></tr>
<tr><td><code>/archipelago/</code></td><td>:5678</td><td></td><td>default</td><td>System endpoints</td></tr>
<tr><td><code>/content</code></td><td>:5678</td><td></td><td>default</td><td>Peer content sharing</td></tr>
<tr><td><code>/dwn</code></td><td>:5678</td><td></td><td>default</td><td>Decentralized Web Node</td></tr>
<tr><td><code>/electrs-status</code></td><td>:5678</td><td></td><td>default</td><td>Electrum sync status (CORS enabled)</td></tr>
<tr><td><code>/lnd-connect-info</code></td><td>:5678</td><td></td><td>default</td><td>LND connection URI (CORS enabled)</td></tr>
</table>
</div>
<div class="card">
<h4>App Proxies — 24 Container Apps</h4>
<p>Every <code>/app/{id}/</code> route proxies into a container. All share a common pattern: strip the upstream <code>X-Frame-Options</code>, set <code>SAMEORIGIN</code>, inject the Nostr provider script, and forward real IP headers.</p>
<table>
<tr><th>App</th><th>Port</th><th>Special Config</th></tr>
<tr><td><code>bitcoin-ui</code></td><td>8334</td><td></td></tr>
<tr><td><code>mempool</code></td><td>4080</td><td>300s timeouts</td></tr>
<tr><td><code>lnd</code></td><td>8081</td><td>300s timeouts</td></tr>
<tr><td><code>electrumx</code></td><td>50002</td><td></td></tr>
<tr><td><code>btcpay</code></td><td>23000</td><td></td></tr>
<tr><td><code>fedimint</code></td><td>8175</td><td>300s timeouts</td></tr>
<tr><td><code>fedimint-gateway</code></td><td>8176</td><td>300s timeouts</td></tr>
<tr><td><code>filebrowser</code></td><td>8083</td><td>10GB uploads, path traversal blocking</td></tr>
<tr><td><code>nextcloud</code></td><td>8085</td><td>300s timeouts</td></tr>
<tr><td><code>vaultwarden</code></td><td>8082</td><td></td></tr>
<tr><td><code>immich</code></td><td>2283</td><td>300s timeouts</td></tr>
<tr><td><code>jellyfin</code></td><td>8096</td><td></td></tr>
<tr><td><code>grafana</code></td><td>3000</td><td></td></tr>
<tr><td><code>portainer</code></td><td>9000</td><td></td></tr>
<tr><td><code>uptime-kuma</code></td><td>3001</td><td></td></tr>
<tr><td><code>searxng</code></td><td>8888</td><td></td></tr>
<tr><td><code>ollama</code></td><td>11434</td><td></td></tr>
<tr><td><code>indeedhub</code></td><td>7777</td><td>URL rewriting, WS, 30-day asset cache</td></tr>
<tr><td><code>homeassistant</code></td><td>8123</td><td>86,400s timeout (persistent)</td></tr>
<tr><td><code>penpot</code></td><td>9001</td><td>300s timeouts</td></tr>
<tr><td><code>photoprism</code></td><td>2342</td><td></td></tr>
<tr><td><code>onlyoffice</code></td><td>8044</td><td></td></tr>
<tr><td><code>endurain</code></td><td>8080</td><td></td></tr>
<tr><td><code>nginx-proxy-manager</code></td><td>8181</td><td></td></tr>
</table>
</div>
<div class="card">
<h4>AIUI Routes (AI Chat Interface)</h4>
<p>The AI chat UI has its own set of proxied API backends — all require a valid session cookie or return <code>401</code>.</p>
<table>
<tr><th>URL Pattern</th><th>Backend</th><th>Timeout</th><th>Purpose</th></tr>
<tr><td><code>/aiui/</code></td><td>Static files</td><td></td><td>Chat UI (no-cache for HTML)</td></tr>
<tr><td><code>/aiui/api/claude/</code></td><td>:3142</td><td>300s read</td><td>Claude proxy (streaming, no buffering)</td></tr>
<tr><td><code>/aiui/api/ollama/</code></td><td>:11434</td><td>300s read</td><td>Local Ollama model (streaming)</td></tr>
<tr><td><code>/aiui/api/openrouter/</code></td><td>openrouter.ai</td><td>120s</td><td>External AI API (SSL passthrough)</td></tr>
<tr><td><code>/aiui/api/web-search</code></td><td>:8888</td><td>30s</td><td>SearXNG search (503 JSON on failure)</td></tr>
</table>
</div>
<!-- ─── Security Headers Comparison ─── -->
<h4>Security Headers — How Archipelago Compares</h4>
<p>Security headers tell the browser what's allowed and what isn't. Here's what each node OS sends:</p>
<table>
<tr>
<th>Header</th>
<th class="archy-col">Archipelago</th>
<th>Umbrel</th>
<th>StartOS</th>
<th>RaspiBlitz</th>
</tr>
<tr>
<td>Content-Security-Policy</td>
<td class="archy-col"><span class="badge badge-green">Full</span> self + WS + frame-src</td>
<td><span class="badge badge-yellow">Basic</span></td>
<td><span class="badge badge-red">None</span></td>
<td><span class="badge badge-red">None</span></td>
</tr>
<tr>
<td>HSTS</td>
<td class="archy-col"><span class="badge badge-green">1 year</span> + includeSubDomains</td>
<td><span class="badge badge-green">Yes</span></td>
<td><span class="badge badge-red">N/A</span> (Tor)</td>
<td><span class="badge badge-red">No</span></td>
</tr>
<tr>
<td>X-Frame-Options</td>
<td class="archy-col"><span class="badge badge-green">SAMEORIGIN</span></td>
<td><span class="badge badge-yellow">Varies</span></td>
<td><span class="badge badge-red">None</span></td>
<td><span class="badge badge-red">None</span></td>
</tr>
<tr>
<td>X-Content-Type-Options</td>
<td class="archy-col"><span class="badge badge-green">nosniff</span></td>
<td><span class="badge badge-green">nosniff</span></td>
<td><span class="badge badge-red">None</span></td>
<td><span class="badge badge-red">None</span></td>
</tr>
<tr>
<td>Permissions-Policy</td>
<td class="archy-col"><span class="badge badge-green">All blocked</span></td>
<td><span class="badge badge-red">None</span></td>
<td><span class="badge badge-red">None</span></td>
<td><span class="badge badge-red">None</span></td>
</tr>
<tr>
<td>Referrer-Policy</td>
<td class="archy-col"><span class="badge badge-green">strict-origin</span></td>
<td><span class="badge badge-red">None</span></td>
<td><span class="badge badge-red">None</span></td>
<td><span class="badge badge-red">None</span></td>
</tr>
<tr>
<td>Rate Limiting</td>
<td class="archy-col"><span class="badge badge-green">Dual-zone</span></td>
<td><span class="badge badge-red">None</span></td>
<td><span class="badge badge-red">None</span></td>
<td><span class="badge badge-red">None</span></td>
</tr>
</table>
<div class="callout callout-success">
<strong>Archipelago leads on security headers.</strong>
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.
</div>
<!-- ─── Unique Features ─── -->
<h4>Unique Nginx Features in Archipelago</h4>
<div class="card-grid">
<div class="card-sm">
<h4>Nostr NIP-07 Injection</h4>
<ul class="item-list">
<li>Every app proxy uses <code>sub_filter</code> to inject <code>nostr-provider.js</code> into <code>&lt;/head&gt;</code></li>
<li>Gives all container apps <code>window.nostr</code> for signing</li>
<li>No other node OS does this — unique to Archipelago</li>
<li><code>Accept-Encoding</code> disabled to enable text rewriting</li>
</ul>
</div>
<div class="card-sm">
<h4>Dual Rate Limit Zones</h4>
<ul class="item-list">
<li><strong>rpc zone:</strong> 20 req/s base, burst of 40 — for API calls</li>
<li><strong>auth zone:</strong> 3 req/s — for login/auth endpoints (brute-force protection)</li>
<li>Returns HTTP 429 on violation</li>
<li>Per-IP tracking with 10MB shared memory zone</li>
</ul>
</div>
<div class="card-sm">
<h4>External Site Proxying</h4>
<ul class="item-list">
<li><code>/ext/botfights/</code>, <code>/ext/484-kitchen/</code>, etc. proxy external HTTPS sites</li>
<li>Strips CORS/COEP/COOP headers for iframe embedding</li>
<li>Rewrites <code>href</code>/<code>src</code> attributes to rebase paths</li>
<li>Standalone proxy servers on ports 89018903</li>
</ul>
</div>
<div class="card-sm">
<h4>FileBrowser Security</h4>
<ul class="item-list">
<li>Path traversal blocked: <code>/\.\.</code> patterns return 403</li>
<li>10GB upload limit (<code>client_max_body_size 10G</code>)</li>
<li><code>proxy_request_buffering off</code> for streaming large uploads</li>
<li>Separate protection for <code>/api/resources/</code> and <code>/api/raw/</code> paths</li>
</ul>
</div>
<div class="card-sm">
<h4>SSL/TLS Configuration</h4>
<ul class="item-list">
<li>TLSv1.2 + TLSv1.3 only (no older protocols)</li>
<li>Modern cipher suite: ECDHE-ECDSA + ECDHE-RSA with AES-GCM</li>
<li>Self-signed certificate at <code>/etc/archipelago/ssl/</code></li>
<li>Dual-server setup: port 80 (HTTP) + port 443 (HTTPS)</li>
</ul>
</div>
<div class="card-sm">
<h4>IndeedhHub Complexity</h4>
<ul class="item-list">
<li>Most complex app proxy: URL rewriting, WebSocket, caching</li>
<li><code>_next/</code> assets cached 30 days with <code>immutable</code></li>
<li>WebSocket at <code>/app/indeedhub/ws/</code> with 24h timeout</li>
<li>Rewrites both single and double quoted <code>href</code>/<code>src</code></li>
</ul>
</div>
</div>
<!-- ─── Config Files Map ─── -->
<h4>Nginx Config File Map</h4>
<div class="card">
<table>
<tr><th>File</th><th>Lines</th><th>Purpose</th></tr>
<tr><td><code>image-recipe/configs/nginx-archipelago.conf</code></td><td>~1,100</td><td>Production config — HTTP + HTTPS servers, all routing</td></tr>
<tr><td><code>image-recipe/configs/snippets/archipelago-https-app-proxies.conf</code></td><td>~400</td><td>HTTPS app proxy blocks (included in main config)</td></tr>
<tr><td><code>image-recipe/configs/snippets/archipelago-pwa.conf</code></td><td>~30</td><td>PWA service worker and manifest caching</td></tr>
<tr><td><code>image-recipe/configs/external-app-proxies.conf</code></td><td>~200</td><td>External site reverse proxies (BotFights, 484 Kitchen)</td></tr>
<tr><td><code>neode-ui/docker/nginx.conf</code></td><td>~60</td><td>Dev Docker config (mock backend on :5959)</td></tr>
<tr><td><code>neode-ui/docker/nginx-demo.conf</code></td><td>~80</td><td>Demo mode config (no security, mock backend)</td></tr>
<tr><td><code>docker/bitcoin-ui/nginx.conf</code></td><td>~50</td><td>Bitcoin UI container — RPC proxy with CORS</td></tr>
<tr><td><code>docker/electrs-ui/nginx.conf</code></td><td>~30</td><td>Electrs UI container — status endpoint</td></tr>
<tr><td><code>docker/lnd-ui/nginx.conf</code></td><td>~30</td><td>LND UI container — connect info</td></tr>
<tr><td><code>indeedhub/nginx.conf</code></td><td>~200</td><td>IndeedhHub container — MinIO, API, relay, SPA</td></tr>
</table>
</div>
<div class="callout callout-learn">
<strong>Why so many nginx configs?</strong>
There are three layers of nginx: (1) the <strong>main server nginx</strong> that routes all traffic, (2) <strong>per-app container nginx</strong> configs inside some containers (bitcoin-ui, electrs-ui, lnd-ui, indeedhub) that serve their own SPAs and proxy to internal services, and (3) <strong>dev/demo nginx</strong> configs for local development. Changes to app routing require updating BOTH the main config AND the relevant container config.
</div>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="data-flow">How Data Flows Through the System</h2>
<p>Let's trace what happens when you click "Install Bitcoin" in the UI:</p>
<div class="card">
<div class="flow-step"><div class="num">1</div><div class="content"><p><strong>You click the Install button</strong> in <code>Marketplace.vue</code>. Vue calls the Pinia store action <code>installPackage('bitcoin-knots')</code></p></div></div>
<div class="flow-step"><div class="num">2</div><div class="content"><p><strong>The store calls the RPC client:</strong> <code>rpcClient.installPackage('bitcoin-knots', 'docker.io/bitcoin/knots:28')</code></p></div></div>
<div class="flow-step"><div class="num">3</div><div class="content"><p><strong>RPC client sends HTTP POST</strong> to <code>/rpc/v1</code> with a session cookie and CSRF token for security</p></div></div>
<div class="flow-step"><div class="num">4</div><div class="content"><p><strong>Nginx receives the request</strong> on port 80, checks rate limits, forwards to the Rust backend on port 5678</p></div></div>
<div class="flow-step"><div class="num">5</div><div class="content"><p><strong>Rust backend validates</strong> — checks your session is valid, CSRF token matches, app ID is safe (no shell injection characters)</p></div></div>
<div class="flow-step"><div class="num">6</div><div class="content"><p><strong>Rust checks dependencies</strong> — if you're installing LND, it checks Bitcoin is already running</p></div></div>
<div class="flow-step"><div class="num">7</div><div class="content"><p><strong>Rust tells Podman to pull the image</strong><code>podman pull docker.io/bitcoin/knots:28</code> (downloads the app)</p></div></div>
<div class="flow-step"><div class="num">8</div><div class="content"><p><strong>Rust creates and starts the container</strong> with all security flags (cap-drop, readonly root, etc.)</p></div></div>
<div class="flow-step"><div class="num">9</div><div class="content"><p><strong>Backend sends a WebSocket update</strong> — the frontend receives a "state changed" event in real time</p></div></div>
<div class="flow-step"><div class="num">10</div><div class="content"><p><strong>Vue reactively updates the UI</strong> — the Marketplace card changes from "Install" to "Running" with no page reload</p></div></div>
</div>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="rpc">RPC: How Frontend Talks to Backend</h2>
<p><strong>RPC</strong> 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.</p>
<div class="callout callout-learn">
<strong>RPC vs REST</strong>
Most web APIs use REST (different URLs for different things: <code>GET /users</code>, <code>POST /users</code>, <code>DELETE /users/5</code>). Archipelago uses RPC instead — every request goes to the same URL (<code>/rpc/v1</code>) and the <em>method name</em> says what to do. It's like having one phone number for a building, and you say who you want to talk to.
</div>
<p>The frontend has a class called <code>RPCClient</code> (in <code>rpc-client.ts</code>) with ~70 methods. Each method maps to a backend function:</p>
<table>
<tr><th>Frontend Method</th><th>Backend Handler</th><th>What It Does</th></tr>
<tr><td><code>rpcClient.login(password)</code></td><td><code>auth.login</code></td><td>Log in with password</td></tr>
<tr><td><code>rpcClient.getServerInfo()</code></td><td><code>system.info</code></td><td>Get server name, version, uptime</td></tr>
<tr><td><code>rpcClient.installPackage(id, image)</code></td><td><code>package.install</code></td><td>Install a container app</td></tr>
<tr><td><code>rpcClient.getBitcoinInfo()</code></td><td><code>bitcoin.info</code></td><td>Get blockchain sync %, block height</td></tr>
<tr><td><code>rpcClient.sendMeshMessage(text)</code></td><td><code>mesh.send</code></td><td>Send a message over LoRa radio</td></tr>
</table>
<h4>Built-in resilience</h4>
<p>The RPC client has built-in protections:</p>
<ul>
<li><strong>Auto-retry</strong> — if a request fails (502/503), it waits and tries again (up to 3 times)</li>
<li><strong>Timeout</strong> — if the backend doesn't respond in 30 seconds, the request fails instead of hanging forever</li>
<li><strong>Session expiry</strong> — if you get a 401 (unauthorized), it redirects to the login page</li>
<li><strong>CSRF protection</strong> — every request includes a security token to prevent cross-site attacks</li>
</ul>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="state">State Management</h2>
<p><strong>State</strong> 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.</p>
<div class="callout callout-learn">
<strong>What is Pinia?</strong>
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.
</div>
<p>Archipelago has <strong>18 Pinia stores</strong> (up from 15 — the "god store" was decomposed):</p>
<div class="card-grid">
<div class="card-sm">
<h4><code>app.ts</code> <span class="badge badge-green">slimmed</span></h4>
<p>Core app state — slimmed down after extracting auth, server, and sync concerns</p>
</div>
<div class="card-sm">
<h4><code>auth.ts</code> <span class="badge badge-green">new</span></h4>
<p>Authentication state machine — login, logout, TOTP, session management</p>
</div>
<div class="card-sm">
<h4><code>server.ts</code> <span class="badge badge-green">new</span></h4>
<p>Server computed state + RPC action proxies (install, restart, update)</p>
</div>
<div class="card-sm">
<h4><code>sync.ts</code> <span class="badge badge-green">new</span></h4>
<p>WebSocket connection + real-time JSON patch (RFC 6902) data sync</p>
</div>
<div class="card-sm">
<h4><code>container.ts</code> <span class="badge badge-green">good</span></h4>
<p>Container lifecycle — running, stopped, installing states (9.2 KB)</p>
</div>
<div class="card-sm">
<h4><code>mesh.ts</code> <span class="badge badge-green">good</span></h4>
<p>LoRa radio state — device, peers, messages, channels (14 KB)</p>
</div>
<div class="card-sm">
<h4><code>appLauncher.ts</code> <span class="badge badge-green">good</span></h4>
<p>App iframe management, Nostr consent, port mapping (11 KB)</p>
</div>
<div class="card-sm">
<h4><code>aiPermissions.ts</code> <span class="badge badge-green">good</span></h4>
<p>AI data access permission management (5.2 KB)</p>
</div>
</div>
<div class="callout callout-success">
<strong>Store decomposition complete.</strong> The old "god store" (<code>app.ts</code>) that handled auth + WebSocket + server data + package management was split into three new focused stores: <code>auth.ts</code> (authentication state machine), <code>server.ts</code> (server state + RPC actions), and <code>sync.ts</code> (WebSocket + data synchronization). The login flow is now: <code>useAuthStore().login()</code><code>useSyncStore().initializeData()</code> + <code>connectWebSocket()</code> → views consume <code>sync.data</code> reactively.
</div>
<h4>WebSocket: real-time updates</h4>
<p>Instead of the frontend asking "has anything changed?" every second (polling), the backend <em>pushes</em> updates to the frontend through a WebSocket — a persistent, two-way connection.</p>
<div class="diagram">
<span class="green">Traditional polling (slow, wasteful):</span>
Frontend: "Anything new?" → Backend: "No" (every 1 second)
Frontend: "Anything new?" → Backend: "No"
Frontend: "Anything new?" → Backend: "Yes! Bitcoin synced!"
<span class="blue">WebSocket (fast, efficient):</span>
Frontend ←→ Backend: persistent connection
Backend: "Bitcoin synced!" → Frontend instantly updates
Backend: "New container started!" → Frontend instantly updates
</div>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="auth">Authentication & Sessions</h2>
<p>When you log in, the backend creates a <strong>session</strong> — a temporary "you're allowed in" token. Here's how it works:</p>
<div class="flow-step"><div class="num">1</div><div class="content"><p><strong>You enter your password</strong> on the login page</p></div></div>
<div class="flow-step"><div class="num">2</div><div class="content"><p><strong>Backend hashes it with bcrypt</strong> — a one-way function that makes it impossible to reverse</p></div></div>
<div class="flow-step"><div class="num">3</div><div class="content"><p><strong>Backend compares the hash</strong> to the stored hash (never compares raw passwords)</p></div></div>
<div class="flow-step"><div class="num">4</div><div class="content"><p><strong>Backend creates a session</strong> — generates a random 256-bit token using a cryptographically secure random number generator</p></div></div>
<div class="flow-step"><div class="num">5</div><div class="content"><p><strong>Session ID sent as a cookie</strong> — the browser stores it and sends it with every request</p></div></div>
<div class="flow-step"><div class="num">6</div><div class="content"><p><strong>CSRF token also sent</strong> — a second token that prevents cross-site request forgery attacks</p></div></div>
<div class="callout callout-info">
<strong>Why two tokens?</strong>
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.
</div>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="security">Security Model</h2>
<p>Archipelago is a defense-in-depth system — multiple layers of security so that if one fails, others still protect you.</p>
<div class="card">
<table>
<tr><th>Layer</th><th>Protection</th><th>Against What</th></tr>
<tr>
<td><span class="badge badge-red">OS</span></td>
<td>UFW firewall, AppArmor profiles</td>
<td>Network attacks, process escape</td>
</tr>
<tr>
<td><span class="badge badge-accent">Nginx</span></td>
<td>Rate limiting, security headers, HSTS</td>
<td>DDoS, XSS, clickjacking</td>
</tr>
<tr>
<td><span class="badge badge-green">Backend</span></td>
<td>CSRF validation, session auth, input sanitization</td>
<td>CSRF, injection, unauthorized access</td>
</tr>
<tr>
<td><span class="badge badge-purple">Containers</span></td>
<td>Capability dropping, readonly root, non-root user</td>
<td>Container escape, privilege escalation</td>
</tr>
<tr>
<td><span class="badge badge-blue">Crypto</span></td>
<td>ChaCha20-Poly1305 encryption, Argon2 key derivation, ed25519 signatures</td>
<td>Data theft, key compromise, impersonation</td>
</tr>
<tr>
<td><span class="badge badge-yellow">Network</span></td>
<td>Tor routing, onion services</td>
<td>Traffic analysis, IP exposure</td>
</tr>
</table>
</div>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="bitcoin">Bitcoin Integration</h2>
<p>Bitcoin is the heart of Archipelago. The backend communicates with Bitcoin Core/Knots using <strong>JSON-RPC</strong> — the same protocol Bitcoin has used since 2009.</p>
<div class="callout callout-warn">
<strong>Critical Rule: Never Use Floating Point for Bitcoin</strong>
Bitcoin amounts are always in <strong>satoshis</strong> (1 BTC = 100,000,000 sats) as <strong>integers</strong>. 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 <code>u64</code> in Rust and <code>BigInt</code> in TypeScript for all Bitcoin amounts.
</div>
<h4>Bitcoin RPC examples</h4>
<pre><code><span class="code-comment">// The backend calls Bitcoin Core like this:</span>
<span class="code-fn">bitcoin_rpc</span>(<span class="code-string">"getblockchaininfo"</span>) <span class="code-comment">→ sync progress, block height</span>
<span class="code-fn">bitcoin_rpc</span>(<span class="code-string">"getnetworkinfo"</span>) <span class="code-comment">→ peer count, version</span>
<span class="code-fn">bitcoin_rpc</span>(<span class="code-string">"getmempoolinfo"</span>) <span class="code-comment">→ unconfirmed transaction count</span>
<span class="code-fn">bitcoin_rpc</span>(<span class="code-string">"estimatesmartfee"</span>, 6) <span class="code-comment">→ fee estimate for 6-block confirmation</span></code></pre>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="federation">Federation & Multi-Node</h2>
<p>Multiple Archipelago nodes can form a <strong>federation</strong> — a trusted network of servers that sync data, share state, and communicate privately.</p>
<div class="diagram">
<span class="highlight">Your Node (.228)</span> ←── Tor ──→ <span class="blue">Friend's Node</span>
│ │
└──── Tor ──→ <span class="green">Office Node</span> ←── Tor ──┘
Each node has:
<span class="purple">Ed25519 identity key</span> (cryptographic identity)
<span class="blue">DID</span> (Decentralized Identifier — like a username that can't be taken away)
<span class="green">Onion address</span> (Tor hidden service — no IP address exposed)
<span class="highlight">DWN</span> (Decentralized Web Node — stores and syncs data)
</div>
<p>Nodes discover each other through <strong>Nostr relays</strong> (publish presence, but never onion addresses — those are exchanged privately via encrypted DMs).</p>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="mesh">Mesh Networking</h2>
<p>Archipelago can communicate over <strong>LoRa radio</strong> — 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.</p>
<div class="analogy">
<p>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.</p>
</div>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="deploy-system">Deploy System</h2>
<p>The deploy script (<code>scripts/deploy-to-target.sh</code>) is how code gets from your development laptop to the live server. It's a ~1,790-line shell script (with shared functions from <code>lib/common.sh</code>) that automates everything:</p>
<div class="flow-step"><div class="num">1</div><div class="content"><p><strong>Pre-flight checks</strong> — verifies SSH connectivity, checks git state, warns about uncommitted changes</p></div></div>
<div class="flow-step"><div class="num">2</div><div class="content"><p><strong>Frontend build</strong> — runs <code>npm run build</code> to compile Vue/TypeScript into static files</p></div></div>
<div class="flow-step"><div class="num">3</div><div class="content"><p><strong>Upload frontend</strong> — rsyncs built files to <code>/opt/archipelago/web-ui/</code> on the server</p></div></div>
<div class="flow-step"><div class="num">4</div><div class="content"><p><strong>Upload Rust source</strong> — rsyncs <code>core/</code> to the server (builds ON the server, not macOS)</p></div></div>
<div class="flow-step"><div class="num">5</div><div class="content"><p><strong>Build on server</strong> — runs <code>cargo build --release</code> on the Linux server</p></div></div>
<div class="flow-step"><div class="num">6</div><div class="content"><p><strong>Sync configs</strong> — copies nginx config, systemd service from <code>image-recipe/configs/</code></p></div></div>
<div class="flow-step"><div class="num">7</div><div class="content"><p><strong>Restart services</strong> — reloads nginx, restarts the Rust backend via systemd</p></div></div>
<div class="flow-step"><div class="num">8</div><div class="content"><p><strong>Health check</strong> — pings <code>/health</code> endpoint to verify everything came back up</p></div></div>
<div class="flow-step"><div class="num">9</div><div class="content"><p><strong>Deploy manifest</strong> — writes a JSON file recording the commit, timestamp, and deploy status</p></div></div>
<div class="callout callout-warn">
<strong>Why build on the server?</strong>
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 <em>source code</em> and compiles on the target machine.
</div>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="iso-build">ISO Build Process</h2>
<p>The ISO build creates the installer that users flash to USB. It's a ~1,870-line script that:</p>
<ol>
<li>Downloads a Debian 12 Live ISO as the base</li>
<li>Creates a Docker container to build a custom root filesystem</li>
<li>Installs Podman, Nginx, and all system dependencies</li>
<li>Captures running container images from the live dev server</li>
<li>Bundles the frontend files, backend binary, and configs</li>
<li>Writes a first-boot script that sets everything up on install</li>
<li>Packages everything into a bootable ISO file</li>
</ol>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="first-boot">First Boot Sequence</h2>
<p>When someone installs the ISO and boots for the first time, <code>first-boot-containers.sh</code> runs automatically and:</p>
<ol>
<li>Generates unique credentials for this installation (Bitcoin RPC password, database passwords)</li>
<li>Sets up swap space based on available RAM</li>
<li>Creates the <code>archy-net</code> container network for inter-container communication</li>
<li>Starts 30+ containers in tiered order (databases first, then Bitcoin, then everything else)</li>
<li>Runs health checks on critical containers</li>
<li>Configures Tor hidden services</li>
</ol>
<!-- ══════════════════════════════════════════════════════════════ -->
<!-- ═══════════════ ARCHITECTURE REVIEW SECTION ═══════════════ -->
<!-- ══════════════════════════════════════════════════════════════ -->
<h2 id="scores">Quality Scores</h2>
<p>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. <strong>Several scores improved</strong> since the last review thanks to major refactoring:</p>
<div class="score-grid">
<div class="score-card">
<div class="label">Rust Error Handling</div>
<div class="score" style="color: var(--green)">A</div>
<p style="font-size:12px;color:var(--text-muted)">Zero unwrap/panic in prod code</p>
</div>
<div class="score-card">
<div class="label">TypeScript Safety</div>
<div class="score" style="color: var(--green)">A</div>
<p style="font-size:12px;color:var(--text-muted)">Strict mode, zero <code>any</code> types</p>
</div>
<div class="score-card">
<div class="label">Security</div>
<div class="score" style="color: var(--green)">A</div>
<p style="font-size:12px;color:var(--text-muted)">33-finding pentest, all remediated</p>
</div>
<div class="score-card">
<div class="label">Frontend Architecture</div>
<div class="score" style="color: var(--green)">A</div>
<p style="font-size:12px;color:var(--text-muted)">God store split, god views split</p>
</div>
<div class="score-card">
<div class="label">Backend Modularity</div>
<div class="score" style="color: var(--green)">A-</div>
<p style="font-size:12px;color:var(--text-muted)">Monoliths split into subdirectories</p>
</div>
<div class="score-card">
<div class="label">Container Security</div>
<div class="score" style="color: var(--green)">A</div>
<p style="font-size:12px;color:var(--text-muted)">Cap-drop, readonly, non-root</p>
</div>
<div class="score-card">
<div class="label">Script Modularity</div>
<div class="score" style="color: var(--yellow)">B-</div>
<p style="font-size:12px;color:var(--text-muted)">Shared lib created, still large scripts</p>
</div>
<div class="score-card">
<div class="label">Test Coverage</div>
<div class="score" style="color: var(--yellow)">B-</div>
<p style="font-size:12px;color:var(--text-muted)">38 frontend + 36 backend test files</p>
</div>
<div class="score-card">
<div class="label">CI/CD</div>
<div class="score" style="color: var(--red)">C</div>
<p style="font-size:12px;color:var(--text-muted)">macOS release CI, no test gating</p>
</div>
<div class="score-card">
<div class="label">Documentation</div>
<div class="score" style="color: var(--green)">A</div>
<p style="font-size:12px;color:var(--text-muted)">This review + MASTER_PLAN + consolidated docs</p>
</div>
<div class="score-card">
<div class="label">Dependency Hygiene</div>
<div class="score" style="color: var(--yellow)">B-</div>
<p style="font-size:12px;color:var(--text-muted)">Floating crypto versions</p>
</div>
<div class="score-card">
<div class="label">Deploy Safety</div>
<div class="score" style="color: var(--green)">A</div>
<p style="font-size:12px;color:var(--text-muted)">Rollback, manifests, health checks, locking</p>
</div>
</div>
<div class="callout callout-success">
<strong>Score improvements since last review (2026-03-20):</strong>
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).
</div>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="whats-good">What's Done Well</h2>
<div class="card">
<h4>Rust: Exceptional Error Discipline</h4>
<p>Zero <code>unwrap()</code> or <code>panic!()</code> in production code. Every fallible operation uses the <code>?</code> operator to propagate errors gracefully. This is rare even in professional Rust codebases.</p>
<h4>Backend Module Architecture (Refactored)</h4>
<p>The backend was comprehensively refactored from monolithic files into domain-focused subdirectories. Previously: <code>package.rs</code> (1,795 lines), <code>federation.rs</code> (810 lines), <code>handler.rs</code> (800+ lines) were all single files. Now: each is a clean directory with focused sub-modules (e.g., <code>package/</code> has config.rs, install.rs, lifecycle.rs, runtime.rs, stacks.rs, dependencies.rs, progress.rs). The RPC layer uses a dedicated <code>dispatcher.rs</code> for routing. All 8 major domains (package, federation, identity, mesh, system, tor, handler, credentials) follow the same <code>mod.rs</code> + <code>handlers.rs</code> pattern.</p>
<h4>Frontend Component Decomposition (Refactored)</h4>
<p>All "god components" were split into sub-views: <code>Web5.vue</code> (3,940 lines) → 14 focused sub-views under <code>views/web5/</code>. <code>Settings.vue</code> (1,792 lines) → 13 sections. <code>Dashboard.vue</code>, <code>Apps.vue</code>, <code>AppDetails.vue</code>, <code>AppSession.vue</code>, <code>Federation.vue</code>, <code>Fleet.vue</code>, <code>Discover.vue</code> — all extracted into subdirectories with focused components. The Pinia god store was decomposed into <code>auth.ts</code>, <code>server.ts</code>, and <code>sync.ts</code>.</p>
<h4>Input Validation is Thorough</h4>
<p>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 <code>rate_limit.rs</code>.</p>
<h4>TypeScript Strict Mode Actually Used</h4>
<p>All 5 strictest compiler flags enabled. Zero <code>any</code> types across 45,500+ lines. Every function has proper types. This prevents entire categories of bugs.</p>
<h4>Container Security is Production-Grade</h4>
<p>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.</p>
<h4>WebSocket Resilience</h4>
<p>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.</p>
<h4>Composables Well-Factored</h4>
<p>11 Vue composables, each focused on one concern (toasts, audio, keyboard, onboarding, controller nav). Clean, reusable, properly scoped. 10 test files for composables.</p>
<h4>Deploy Safety Features</h4>
<p>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 (<code>scripts/lib/common.sh</code>) eliminates function duplication.</p>
<h4>Monitoring & Telemetry System</h4>
<p>New <code>monitoring/</code> module (1,380 LOC) with metrics collection, alert generation, persistent storage, beta telemetry reporting, and notification dispatch. Production-grade observability for the beta phase.</p>
<h4>PodmanClient Uses REST API Socket</h4>
<p>The container management layer communicates with Podman via its async REST API unix socket (<code>/run/user/{UID}/podman/podman.sock</code>), not CLI. This is faster, more reliable, and avoids shell injection risks.</p>
<h4>Full Security Audit Completed</h4>
<p>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.</p>
</div>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="whats-wrong">What Needs Fixing</h2>
<h3>Production Reliability <span class="badge badge-red">P0 — blocks beta</span></h3>
<div class="card">
<h4>P0-1. Health RPC endpoint has no handler</h4>
<p><strong>What:</strong> <code>"health"</code> is listed in <code>UNAUTHENTICATED_METHODS</code> but has no match handler — returns "Unknown method" error instead of actual health status.</p>
<p><strong>Impact:</strong> Frontend, load balancers, and orchestrators can't verify the backend is actually healthy. System appears unhealthy when it's fine.</p>
<p><strong>Fix:</strong> Add handler that checks crash recovery status, Podman responsiveness, and service readiness.</p>
</div>
<div class="card">
<h4>P0-2. Zero container health checks across all 30 containers</h4>
<p><strong>What:</strong> <code>first-boot-containers.sh</code> creates 30+ containers with <code>--restart unless-stopped</code> but zero <code>--health-cmd</code> flags. Crashed containers restart endlessly in a hammer loop.</p>
<p><strong>Impact:</strong> Silent failures — a broken app looks "running" but returns errors. No way for the backend to distinguish healthy from crashed.</p>
<p><strong>Fix:</strong> Add <code>--health-cmd</code> with appropriate checks (HTTP, TCP, CLI) to every container.</p>
</div>
<div class="card">
<h4>P0-3. Backup restore has no pre-validation or atomic rollback</h4>
<p><strong>What:</strong> <code>restore_full_backup()</code> 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.</p>
<p><strong>Impact:</strong> A corrupted backup can brick a fresh install. Data loss on partial restore failure.</p>
<p><strong>Fix:</strong> Extract to staging directory, validate required files, atomic rename, rollback on failure.</p>
</div>
<div class="card">
<h4>P0-4. Unauthenticated nginx endpoints missing protections</h4>
<p><strong>What:</strong> <code>/archipelago/</code>, <code>/content</code>, <code>/dwn</code> endpoints (used for Tor P2P federation) have no timeout, body size limit, or rate limiting.</p>
<p><strong>Impact:</strong> Vulnerable to slow-loris attacks, payload flooding, and connection exhaustion via Tor.</p>
<p><strong>Fix:</strong> Add <code>proxy_connect_timeout</code>, <code>client_max_body_size 10m</code>, and <code>limit_req</code> to all three locations.</p>
</div>
<h3>Critical Issues <span class="badge badge-yellow">recently resolved</span></h3>
<div class="card">
<h4><span class="check">&#10003;</span> RESOLVED: package.rs was 1,795 lines — split into 7 files</h4>
<p><strong>Before:</strong> Single monolithic file handling all container operations.</p>
<p><strong>After:</strong> Split into <code>package/config.rs</code> (692 LOC), <code>package/install.rs</code> (467 LOC), <code>package/lifecycle.rs</code>, <code>package/runtime.rs</code> (417 LOC), <code>package/stacks.rs</code> (356 LOC), <code>package/dependencies.rs</code> (242 LOC), <code>package/progress.rs</code> (140 LOC). Each file has one clear responsibility.</p>
</div>
<div class="card">
<h4><span class="check">&#10003;</span> RESOLVED: Web5.vue was 3,940 lines — split into 14 sub-views</h4>
<p><strong>Before:</strong> One massive component with 17 sections.</p>
<p><strong>After:</strong> Extracted to <code>views/web5/</code> with: Web5.vue (main), Web5ConnectedNodes, Web5CredentialsSummary, Web5DWN, Web5Domains, Web5Identities, Web5NodeVisibility, Web5NostrRelays, Web5QuickActions, Web5SendReceiveModals, Web5SharedContent, Web5Wallet, types.ts, utils.ts.</p>
</div>
<div class="card">
<h4><span class="check">&#10003;</span> RESOLVED: useAppStore was a "god store" — split into 3 focused stores</h4>
<p><strong>Before:</strong> One store handling auth, WebSocket, server data, and package management.</p>
<p><strong>After:</strong> Decomposed into <code>auth.ts</code> (login/logout/TOTP/sessions), <code>server.ts</code> (server state + RPC actions), <code>sync.ts</code> (WebSocket + JSON patch data sync). <code>app.ts</code> is now a thin data store.</p>
</div>
<div class="card">
<h4><span class="check">&#10003;</span> RESOLVED: Shell scripts had no shared library</h4>
<p><strong>Before:</strong> Duplicated functions across deploy, first-boot, and helper scripts.</p>
<p><strong>After:</strong> <code>scripts/lib/common.sh</code> provides shared functions: colored logging, SSH wrappers (<code>ssh_cmd</code>, <code>scp_cmd</code>), health checks, disk checks, memory limits. Sourced by all deployment scripts.</p>
</div>
<h3>Remaining Critical Issues <span class="badge badge-red">fix now</span></h3>
<div class="card">
<h4>1. Test coverage exists but has gaps</h4>
<p><strong>What:</strong> 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.</p>
<p><strong>Fix:</strong> Add integration tests for critical paths (auth flow, container lifecycle, federation handshake). Add CI that runs <code>cargo test</code> + <code>npm test</code> on every push.</p>
</div>
<h3>High Priority <span class="badge badge-yellow">fix soon</span></h3>
<div class="card">
<h4>P1-A. Nostr client.connect() hangs indefinitely (no timeout) <span class="badge badge-green">FIXED</span></h4>
<p><strong>What:</strong> 6 calls to <code>client.connect().await</code> across <code>identity_manager.rs</code>, <code>nostr_discovery.rs</code>, and <code>marketplace.rs</code> had no timeout wrapper. If a relay is down, peer discovery hangs forever.</p>
<p><strong>Fix:</strong> All 6 calls wrapped in <code>tokio::time::timeout(Duration::from_secs(10), ...)</code>. (v1.3.1, 2026-03-25)</p>
</div>
<div class="card">
<h4>P1-B. Rate limiter memory grows unbounded</h4>
<p><strong>What:</strong> <code>EndpointRateLimiter::cleanup()</code> and <code>LoginRateLimiter</code> cleanup methods exist but are never spawned. HashMap of (method, IP) entries grows forever.</p>
<p><strong>Fix:</strong> Spawn cleanup task every 5 minutes in <code>RpcHandler::new()</code>.</p>
</div>
<div class="card">
<h4>P1-C. Systemd service missing resource limits</h4>
<p><strong>What:</strong> No <code>MemoryMax</code>, <code>LimitNOFILE</code>, or <code>TasksMax</code> in <code>archipelago.service</code>. A memory leak in the backend can OOM-kill the entire system.</p>
<p><strong>Fix:</strong> Add <code>MemoryMax=4G</code>, <code>LimitNOFILE=65535</code>, <code>TasksMax=2048</code>.</p>
</div>
<div class="card">
<h4>P1-D. Container images using :latest tag (7 instances) <span class="badge badge-green">FIXED</span></h4>
<p><strong>What:</strong> Several containers in <code>first-boot-containers.sh</code> and the ISO build pulled floating tags — no exact version pinning.</p>
<p><strong>Impact:</strong> Two machines installed a week apart may have different Bitcoin node versions. Supply chain risk.</p>
<p><strong>Fix:</strong> All 15 floating tags in <code>image-versions.sh</code> pinned to exact patch versions (e.g., postgres:15→15.17, redis:7→7.4.8, nginx:alpine→1.29.6-alpine). DWN pinned by SHA256 digest. (v1.3.1, 2026-03-25)</p>
</div>
<div class="card">
<h4>P1-E. WebSocket reconnect doesn't refresh full state</h4>
<p><strong>What:</strong> After a WebSocket disconnect (5+ minutes), the UI shows stale data. Reconnection applies patches to an outdated base state instead of fetching fresh data.</p>
<p><strong>Fix:</strong> On reconnect, call <code>server.get-state</code> RPC to refresh full state before accepting patches.</p>
</div>
<div class="card">
<h4>P1-F. No global Vue error handler</h4>
<p><strong>What:</strong> No <code>app.config.errorHandler</code> in <code>main.ts</code>. Component errors silently log to console — user sees blank screen with no recovery path.</p>
<p><strong>Fix:</strong> Add error handler that shows user-visible toast and logs structured error.</p>
</div>
<div class="card">
<h4>5. Cryptographic dependency versions not pinned exactly <span class="badge badge-green">FIXED</span></h4>
<p><strong>What:</strong> <code>zeroize = "1.7"</code>, <code>chacha20poly1305 = "0.10"</code>, <code>ed25519-dalek = "2.1"</code> used floating versions.</p>
<p><strong>Why it's bad:</strong> 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.</p>
<p><strong>Fix:</strong> All 12 crypto deps pinned to exact versions from Cargo.lock: ed25519-dalek=2.2.0, zeroize=1.8.2, chacha20poly1305=0.10.1, sha2=0.10.9, hmac=0.12.1, argon2=0.5.3, aes-gcm=0.10.3, etc. (v1.3.1, 2026-03-25)</p>
</div>
<div class="card">
<h4>6. No frontend-backend type synchronization</h4>
<p><strong>What:</strong> TypeScript types in <code>types/api.ts</code> are manually maintained copies of Rust structs. If the backend changes a field name, the frontend doesn't know until runtime.</p>
<p><strong>Why it's bad:</strong> Types can drift apart silently. A backend developer renames <code>sync_progress</code> to <code>syncProgress</code> and the frontend breaks in production.</p>
<p><strong>Fix:</strong> Generate TypeScript types from Rust structs (using <code>ts-rs</code> or a JSON Schema).</p>
</div>
<div class="card">
<h4>7. Container metadata duplicated in 3 places</h4>
<p><strong>What:</strong> App configuration (ports, volumes, env vars) exists in: <code>package.rs</code> (RPC handler), <code>docker_packages.rs</code> (metadata reader), <code>health_monitor.rs</code> (startup tiers).</p>
<p><strong>Why it's bad:</strong> Adding a new app means updating 3 files. If you forget one, the app partially works but something is wrong.</p>
<p><strong>Fix:</strong> Single app config source (manifest YAML or a shared Rust module) that all three consumers read from.</p>
</div>
<div class="card">
<h4>8. Deploy and ISO build scripts are still 1,700+ lines each</h4>
<p><strong>What:</strong> Two monolithic shell scripts (deploy: ~1,790 lines, ISO build: ~1,870 lines) handle dozens of responsibilities each. Shared functions have been extracted to <code>scripts/lib/common.sh</code>, but the scripts themselves are still large.</p>
<p><strong>Improvement:</strong> <code>scripts/lib/common.sh</code> 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.</p>
<p><strong>Next step:</strong> Split deploy into modules: <code>deploy-frontend.sh</code>, <code>deploy-backend.sh</code>, <code>sync-configs.sh</code>. Split ISO build into <code>lib/rootfs.sh</code>, <code>lib/components.sh</code>, <code>lib/installer-env.sh</code>.</p>
</div>
<h3>Medium Priority <span class="badge badge-blue">improve over time</span></h3>
<div class="card">
<h4>9. App integration requires updates in 6+ locations</h4>
<p><strong>What:</strong> Adding a new app to Archipelago requires manual changes in: manifest YAML, <code>package.rs</code> (backend config), <code>docker_packages.rs</code> (metadata), nginx config (routing), <code>Marketplace.vue</code> (frontend listing), <code>appLauncher.ts</code> (port mapping), <code>first-boot-containers.sh</code> (first boot), <code>build-auto-installer-iso.sh</code> (ISO capture).</p>
<p><strong>Fix:</strong> Move toward a single manifest file per app that drives all of these automatically.</p>
</div>
<div class="card">
<h4>10. CI/CD pipeline is minimal <span class="badge badge-green">FIXED</span></h4>
<p><strong>What:</strong> One GitHub Action builds macOS release binaries on tag push. No tests run in CI. No linting. No Linux build or deploy automation.</p>
<p><strong>Fix:</strong> Added <code>.github/workflows/ci.yml</code> with two parallel jobs: Rust (fmt check + clippy -D warnings + tests) and Frontend (npm ci + type-check + build). Runs on push to main and all PRs. (v1.3.1, 2026-03-25)</p>
</div>
<div class="card">
<h4>11. Session persistence uses blocking I/O</h4>
<p><strong>What:</strong> On startup, <code>session.rs</code> reads <code>sessions.json</code> using synchronous (blocking) file I/O in an async context.</p>
<p><strong>Fix:</strong> Use <code>tokio::fs::read_to_string</code> for non-blocking I/O at startup.</p>
</div>
<div class="card">
<h4>12. Inconsistent loading state patterns in frontend</h4>
<p><strong>What:</strong> Some components use <code>loading</code>, others <code>isLoading</code>, others <code>loadingApps</code>. No shared composable.</p>
<p><strong>Fix:</strong> Create a <code>useAsyncState</code> composable that standardizes loading/error/data patterns.</p>
</div>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="refactor-plan">Refactoring Priorities</h2>
<p>Ordered by impact. <strong>8 of 12 items completed</strong> since the previous review — significant progress:</p>
<table>
<tr><th>#</th><th>Task</th><th>Impact</th><th>Effort</th><th>Status</th></tr>
<tr style="opacity: 0.5">
<td>1</td>
<td><s>Split <code>package.rs</code> (1,795 lines) into focused files</s></td>
<td><span class="badge badge-red">high</span></td>
<td>2-3 days</td>
<td><span class="badge badge-green">DONE</span></td>
</tr>
<tr style="opacity: 0.5">
<td>2</td>
<td><s>Split <code>useAppStore</code> into auth/server/sync</s></td>
<td><span class="badge badge-red">high</span></td>
<td>2 days</td>
<td><span class="badge badge-green">DONE</span></td>
</tr>
<tr style="opacity: 0.5">
<td>3</td>
<td><s>Add CI pipeline (clippy + type-check + basic tests)</s></td>
<td><span class="badge badge-red">high</span></td>
<td>1 day</td>
<td><span class="badge badge-green">DONE</span></td>
</tr>
<tr style="opacity: 0.5">
<td>4</td>
<td><s>Split <code>Web5.vue</code> (3,940 lines) into sub-views</s></td>
<td><span class="badge badge-yellow">medium</span></td>
<td>3 days</td>
<td><span class="badge badge-green">DONE</span></td>
</tr>
<tr style="opacity: 0.5">
<td>5</td>
<td><s>Pin all crypto dependency versions exactly</s></td>
<td><span class="badge badge-yellow">medium</span></td>
<td>1 hour</td>
<td><span class="badge badge-green">DONE</span></td>
</tr>
<tr style="opacity: 0.5">
<td>6</td>
<td><s>Extract shared shell library (<code>lib/common.sh</code>)</s></td>
<td><span class="badge badge-yellow">medium</span></td>
<td>1 day</td>
<td><span class="badge badge-green">DONE</span></td>
</tr>
<tr>
<td>7</td>
<td>Consolidate container metadata to single source</td>
<td><span class="badge badge-yellow">medium</span></td>
<td>2 days</td>
<td><span class="badge badge-yellow">TODO</span></td>
</tr>
<tr>
<td>8</td>
<td>Generate TypeScript types from Rust structs</td>
<td><span class="badge badge-yellow">medium</span></td>
<td>1 day</td>
<td><span class="badge badge-blue">TODO</span></td>
</tr>
<tr>
<td>9</td>
<td>Split deploy/ISO scripts into modules</td>
<td><span class="badge badge-blue">low</span></td>
<td>2 days</td>
<td><span class="badge badge-blue">POST-BETA</span></td>
</tr>
<tr>
<td>10</td>
<td>Add integration tests for critical paths</td>
<td><span class="badge badge-red">high</span></td>
<td>3 days</td>
<td><span class="badge badge-red">TODO</span></td>
</tr>
<tr style="opacity: 0.5">
<td>11</td>
<td><s>Split large backend files (federation, identity, handler, system, tor)</s></td>
<td><span class="badge badge-yellow">medium</span></td>
<td>2 days</td>
<td><span class="badge badge-green">DONE</span></td>
</tr>
<tr style="opacity: 0.5">
<td>12</td>
<td><s>Split large Vue views (Settings 1,792, Mesh, Dashboard, Apps, etc.)</s></td>
<td><span class="badge badge-blue">low</span></td>
<td>2 days</td>
<td><span class="badge badge-green">DONE</span></td>
</tr>
</table>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="tech-debt">Technical Debt Map</h2>
<p>A visual summary of where debt lives in the codebase. <strong>Many red items from the previous review are now green</strong> after refactoring:</p>
<div class="diagram">
<span class="highlight">BACKEND (Rust) — 213 files, ~45K LOC</span>
<span class="green">████████</span> package/ (2,248 lines in 7 focused files — was 1,795 god file)
<span class="green">████████</span> api/handler/ (896 lines in 6 files — was 800+ god file)
<span class="green">██████</span> federation/, identity/, system/, tor/ (all split from monoliths)
<span class="green">██████</span> credentials/ (5 files), monitoring/ (7 files) — new modules
<span class="yellow">██████</span> lnd/ (1,092 lines in 5 files — could split further)
<span class="yellow">████</span> mesh/ (6,000 lines — large but domain-appropriate)
<span class="green">████</span> session.rs (622), health_monitor.rs (731) — clean single files
<span class="green">████</span> rate_limit.rs (191) — new, focused
<span class="green">██</span> container, security, performance crates (clean)
<span class="highlight">FRONTEND (Vue + TS) — 232 files, ~45.5K LOC</span>
<span class="green">████████</span> web5/ (14 sub-views — was 3,940-line god component)
<span class="green">████████</span> settings/ (13 sections — was 1,792-line god component)
<span class="green">██████</span> apps/, dashboard/, federation/, fleet/, discover/ (all split)
<span class="green">████</span> auth.ts + server.ts + sync.ts (was god store)
<span class="green">██</span> rpc-client.ts (well-designed), 11 composables (clean)
<span class="green"></span> Type safety (excellent), 38+ test files
<span class="highlight">SCRIPTS (Shell) — ~40 scripts</span>
<span class="yellow">████████████</span> deploy-to-target.sh (~1,790 lines — still large)
<span class="yellow">████████████</span> build-auto-installer-iso.sh (~1,870 lines — still large)
<span class="yellow">██████</span> first-boot-containers.sh (~935 lines, version mismatches)
<span class="green">████</span> scripts/lib/common.sh — shared library (new)
<span class="green">████</span> image-versions.sh — centralized pinning (new)
<span class="green">██</span> Test scripts (well-organized)
<span class="highlight">ARCHITECTURE</span>
<span class="yellow">██████</span> Tests: 74+ files but gaps in integration coverage
<span class="green">██████</span> CI: cargo fmt + clippy + tests, frontend type-check + build
<span class="yellow">████</span> Manual type sync (Rust ↔ TypeScript)
<span class="yellow">████</span> App integration requires 6+ file changes
<span class="green">████</span> Crypto deps pinned to exact versions
<span class="green">████</span> Security model (pentest completed, rate limiting, CSRF)
<span class="green">████</span> Deploy safety (rollback, manifests, locking, health checks)
<span class="green">████</span> Module architecture (all god files eliminated)
<span class="green">██</span> PodmanClient (REST API socket, not CLI)
<span class="green">██</span> Monitoring & telemetry system (production-ready)
<span class="code-comment">Legend: <span class="red">██</span> Critical <span class="yellow">██</span> Needs attention <span class="green">██</span> Good</span>
<span class="code-comment">Progress: <span class="green">████████████████████████████████████████████████</span> ~85% green (was ~40%)</span>
</div>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="learning">Recommended Learning Path</h2>
<p>If you want to understand this codebase deeply and become proficient in all the technologies, study in this order:</p>
<div class="card">
<h4>Phase 1: Foundations (Weeks 1-4)</h4>
<ol>
<li><strong>Linux basics</strong> — commands, file permissions, processes, systemd</li>
<li><strong>Git</strong> — branches, commits, diffs, rebasing</li>
<li><strong>HTML/CSS/JavaScript</strong> — the building blocks of web UIs</li>
<li><strong>TypeScript</strong> — JavaScript with type safety (read the official handbook)</li>
</ol>
</div>
<div class="card">
<h4>Phase 2: Frontend (Weeks 5-8)</h4>
<ol>
<li><strong>Vue 3 Composition API</strong><code>ref</code>, <code>computed</code>, <code>watch</code>, <code>onMounted</code></li>
<li><strong>Pinia</strong> — state management (read <code>stores/container.ts</code> as a good example)</li>
<li><strong>Vue Router</strong> — URL-to-component mapping</li>
<li><strong>Tailwind CSS</strong> — utility-first CSS framework</li>
<li><strong>Vite</strong> — the build tool that bundles everything</li>
</ol>
</div>
<div class="card">
<h4>Phase 3: Backend (Weeks 9-14)</h4>
<ol>
<li><strong>Rust basics</strong> — ownership, borrowing, lifetimes, pattern matching (read "The Rust Book")</li>
<li><strong>Async Rust with Tokio</strong><code>async/await</code>, futures, <code>tokio::spawn</code></li>
<li><strong>Hyper</strong> — the HTTP server library (read <code>server.rs</code>)</li>
<li><strong>Serde</strong> — JSON serialization/deserialization</li>
<li><strong>Error handling</strong><code>anyhow</code>, <code>thiserror</code>, the <code>?</code> operator</li>
</ol>
</div>
<div class="card">
<h4>Phase 4: Infrastructure (Weeks 15-18)</h4>
<ol>
<li><strong>Containers</strong> — Docker/Podman concepts (images, containers, volumes, networks)</li>
<li><strong>Nginx</strong> — reverse proxy, location blocks, upstream servers</li>
<li><strong>Shell scripting</strong> — bash/zsh, <code>set -e</code>, functions, trap</li>
<li><strong>systemd</strong> — service management, unit files, journalctl</li>
<li><strong>Networking</strong> — TCP/IP, DNS, ports, firewalls (UFW)</li>
</ol>
</div>
<div class="card">
<h4>Phase 5: Bitcoin & Crypto (Weeks 19-24)</h4>
<ol>
<li><strong>Bitcoin protocol</strong> — blocks, transactions, UTXOs, mining (read "Mastering Bitcoin")</li>
<li><strong>Lightning Network</strong> — payment channels, routing, invoices</li>
<li><strong>Cryptography</strong> — hashing, symmetric/asymmetric encryption, digital signatures</li>
<li><strong>Tor</strong> — onion routing, hidden services, SOCKS5 proxy</li>
<li><strong>Nostr</strong> — decentralized messaging protocol, NIPs</li>
<li><strong>DIDs</strong> — Decentralized Identifiers, Verifiable Credentials</li>
</ol>
</div>
<div class="callout callout-success">
<strong>Recommended first files to read</strong>
<ol>
<li><code>neode-ui/src/stores/auth.ts</code> — Clean authentication state machine (new, focused store)</li>
<li><code>neode-ui/src/stores/sync.ts</code> — WebSocket + JSON patch data sync (new)</li>
<li><code>neode-ui/src/api/rpc-client.ts</code> — Well-designed API client with retry logic</li>
<li><code>core/archipelago/src/api/rpc/dispatcher.rs</code> — How RPC routing works (new)</li>
<li><code>core/archipelago/src/api/rpc/package/install.rs</code> — App install flow (focused)</li>
<li><code>core/archipelago/src/session.rs</code> — Auth flow in Rust with crypto</li>
<li><code>core/container/src/podman_client.rs</code> — How Rust talks to Podman</li>
<li><code>image-recipe/configs/nginx-archipelago.conf</code> — The full routing map</li>
</ol>
</div>
<!-- ══════════════════════════════════════════════════════ -->
<h2 id="glossary">Glossary</h2>
<table>
<tr><th>Term</th><th>What It Means</th></tr>
<tr><td><strong>API</strong></td><td>Application Programming Interface — a defined way for two programs to talk to each other</td></tr>
<tr><td><strong>Async/Await</strong></td><td>A way to write code that waits for slow things (network, disk) without blocking other work</td></tr>
<tr><td><strong>Backend</strong></td><td>The server-side code that runs on the machine (not visible to users)</td></tr>
<tr><td><strong>Container</strong></td><td>An isolated environment for running an app, like a lightweight virtual machine</td></tr>
<tr><td><strong>Composable</strong></td><td>A reusable piece of logic in Vue (similar to React hooks)</td></tr>
<tr><td><strong>CSRF</strong></td><td>Cross-Site Request Forgery — an attack where a malicious site tricks your browser into sending requests</td></tr>
<tr><td><strong>Crate</strong></td><td>A Rust package (like npm package for JavaScript)</td></tr>
<tr><td><strong>DID</strong></td><td>Decentralized Identifier — a self-owned digital identity (no central authority controls it)</td></tr>
<tr><td><strong>DWN</strong></td><td>Decentralized Web Node — personal data storage that syncs across your devices</td></tr>
<tr><td><strong>Frontend</strong></td><td>The browser-side code that users see and interact with</td></tr>
<tr><td><strong>ISO</strong></td><td>A disk image file — like a digital copy of an installation CD</td></tr>
<tr><td><strong>JWT</strong></td><td>JSON Web Token — a compact way to pass verified identity between systems</td></tr>
<tr><td><strong>LoRa</strong></td><td>Long Range radio — low-power wireless communication over several kilometers</td></tr>
<tr><td><strong>Nginx</strong></td><td>A web server that also works as a reverse proxy (routes traffic to the right service)</td></tr>
<tr><td><strong>Nostr</strong></td><td>A decentralized messaging protocol using public/private key pairs</td></tr>
<tr><td><strong>Onion Service</strong></td><td>A Tor hidden service — a server accessible only through the Tor network (no IP address)</td></tr>
<tr><td><strong>Pinia</strong></td><td>Vue's official state management library (successor to Vuex)</td></tr>
<tr><td><strong>Podman</strong></td><td>A container runtime like Docker, but rootless (more secure)</td></tr>
<tr><td><strong>RPC</strong></td><td>Remote Procedure Call — calling a function on another computer over the network</td></tr>
<tr><td><strong>Reactive</strong></td><td>Data that automatically updates the UI when it changes (core Vue concept)</td></tr>
<tr><td><strong>Reverse Proxy</strong></td><td>A server that sits between clients and backend servers, forwarding requests</td></tr>
<tr><td><strong>Rust</strong></td><td>A systems programming language focused on safety and performance</td></tr>
<tr><td><strong>SPA</strong></td><td>Single Page Application — a web app that loads once and dynamically updates content</td></tr>
<tr><td><strong>Satoshi (sat)</strong></td><td>The smallest unit of Bitcoin. 1 BTC = 100,000,000 sats</td></tr>
<tr><td><strong>systemd</strong></td><td>Linux's service manager — starts, stops, and monitors background services</td></tr>
<tr><td><strong>Tokio</strong></td><td>Rust's async runtime — handles thousands of concurrent operations efficiently</td></tr>
<tr><td><strong>Tor</strong></td><td>The Onion Router — anonymizes internet traffic by routing through multiple relays</td></tr>
<tr><td><strong>TypeScript</strong></td><td>JavaScript with static types — catches bugs at compile time instead of runtime</td></tr>
<tr><td><strong>Vue 3</strong></td><td>A JavaScript framework for building reactive user interfaces</td></tr>
<tr><td><strong>WebSocket</strong></td><td>A persistent, two-way connection between browser and server for real-time data</td></tr>
</table>
<hr>
<p style="text-align:center;color:var(--text-muted);font-size:13px;margin-top:48px;">
Architecture Review — Archipelago v0.1.0-beta — Updated 2026-03-22<br>
~45,000 lines Rust (213 files) &middot; ~45,500 lines TypeScript/Vue (232 files) &middot; ~40 shell scripts
</p>
</main>
<script>
// Highlight active nav link on scroll
const sections = document.querySelectorAll('h2[id], h3[id]');
const navLinks = document.querySelectorAll('nav a');
function updateActiveNav() {
let current = '';
sections.forEach(section => {
const rect = section.getBoundingClientRect();
if (rect.top <= 100) current = section.id;
});
navLinks.forEach(link => {
link.classList.toggle('active', link.getAttribute('href') === '#' + current);
});
}
window.addEventListener('scroll', updateActiveNav, { passive: true });
updateActiveNav();
</script>
<script>
function toggleTheme() {
const html = document.documentElement;
const isLight = html.getAttribute('data-theme') === 'light';
const next = isLight ? 'dark' : 'light';
html.setAttribute('data-theme', next);
localStorage.setItem('archy-review-theme', next);
updateToggleUI(next);
}
function updateToggleUI(theme) {
const icon = document.getElementById('theme-icon');
const label = document.getElementById('theme-label');
if (theme === 'light') {
icon.innerHTML = '&#9728;';
label.textContent = 'Dark mode';
} else {
icon.innerHTML = '&#9790;';
label.textContent = 'Light mode';
}
}
(function() {
const saved = localStorage.getItem('archy-review-theme');
const preferred = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
const theme = saved || preferred;
if (theme === 'light') {
document.documentElement.setAttribute('data-theme', 'light');
}
updateToggleUI(theme);
})();
</script>
</body>
</html>