Files
archy/docs/container-architecture.html
Dorian c917814d32
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Has been cancelled
refactor: migrate container registry from 80.71.235.15:3000 to git.tx1138.com/lfg2025
All hardcoded references to the old IP-based registry replaced across
Rust backend, Vue frontend, shell scripts, Dockerfiles, CI, and docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 09:33:10 -04:00

4280 lines
304 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Archipelago System Architecture</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'SF Pro', system-ui, sans-serif; background: #0a0e1a; color: #e0e0e0; min-height: 100vh; line-height: 1.65; }
.section, .card, .proto-card, .sec-card, .glossary-item, .popover { overflow-wrap: break-word; word-break: break-word; }
.proto-details code, .layer-details code, .card-details code, .sec-desc code, .boot-desc code { word-break: break-all; }
.sticky-top { position: sticky; top: 0; z-index: 100; }
.header { padding: 28px 32px 20px; background: linear-gradient(135deg, #0f1629 0%, #1a1040 100%); border-bottom: 1px solid rgba(99,102,241,0.2); position: relative; }
.header h1 { font-size: 26px; font-weight: 700; color: #fff; letter-spacing: -0.5px; }
.header p { font-size: 13px; color: #8b8fa3; margin-top: 4px; }
/* Navigation tabs */
.nav { display: flex; gap: 0; background: #0d1120; border-bottom: 1px solid rgba(255,255,255,0.06); overflow-x: auto; }
.nav-btn { padding: 12px 20px; border: none; background: transparent; color: #6b7280; cursor: pointer; font-size: 13px; font-weight: 500; transition: all 0.2s; white-space: nowrap; border-bottom: 2px solid transparent; }
.nav-btn:hover { color: #c4c8e0; background: rgba(255,255,255,0.02); }
.nav-btn.active { color: #a5b4fc; border-bottom-color: #6366f1; background: rgba(99,102,241,0.05); }
/* Sections */
.section { display: none; padding: 24px 32px; }
.section.active { display: block; }
/* Filter controls */
.controls { padding: 16px 0; display: flex; gap: 8px; flex-wrap: wrap; }
.filter-btn { padding: 6px 14px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.1); background: transparent; color: #8b8fa3; cursor: pointer; font-size: 12px; transition: all 0.2s; }
.filter-btn:hover { border-color: rgba(99,102,241,0.4); color: #c4c8e0; }
.filter-btn.active { background: rgba(99,102,241,0.15); border-color: rgba(99,102,241,0.5); color: #a5b4fc; }
/* Stats bar */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; margin-bottom: 20px; }
.stat { background: rgba(255,255,255,0.03); border-radius: 8px; padding: 12px 16px; text-align: center; border: 1px solid rgba(255,255,255,0.04); }
.stat-value { font-size: 22px; font-weight: 700; color: #fff; }
.stat-label { font-size: 11px; color: #6b7280; margin-top: 2px; }
/* Cards */
.tier-section { margin-bottom: 20px; }
.tier-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; padding: 8px 0; display: flex; align-items: center; gap: 8px; }
.tier-label::after { content: ''; flex: 1; height: 1px; background: rgba(255,255,255,0.06); }
.tier-0 .tier-label { color: #f59e0b; }
.tier-1 .tier-label { color: #ef4444; }
.tier-2 .tier-label { color: #8b5cf6; }
.tier-3 .tier-label { color: #10b981; }
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 10px; align-items: start; }
.cards.all-expanded { align-items: stretch; }
.cards.all-expanded .card { display: flex; flex-direction: column; }
.cards.all-expanded .card-details { flex: 1; }
.cards.all-expanded .detail-row { align-items: flex-start; }
.card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); border-radius: 10px; padding: 14px 16px; cursor: pointer; transition: all 0.2s; position: relative; overflow: hidden; overflow-wrap: break-word; word-break: break-word; }
.card:hover { background: rgba(255,255,255,0.06); border-color: rgba(255,255,255,0.12); transform: translateY(-1px); }
.card.expanded { background: rgba(255,255,255,0.05); border-color: rgba(99,102,241,0.3); }
.card.highlight { animation: pulse 1s ease-in-out; }
@keyframes pulse { 0%,100% { box-shadow: none; } 50% { box-shadow: 0 0 20px rgba(99,102,241,0.3); } }
.card-header { display: flex; justify-content: space-between; align-items: center; }
.card-name { font-size: 14px; font-weight: 600; color: #fff; }
.card-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; font-weight: 500; }
.badge-red { background: rgba(239,68,68,0.15); color: #fca5a5; }
.badge-green { background: rgba(16,185,129,0.15); color: #6ee7b7; }
.badge-amber { background: rgba(245,158,11,0.15); color: #fcd34d; }
.badge-purple { background: rgba(139,92,246,0.15); color: #c4b5fd; }
.badge-blue { background: rgba(99,102,241,0.15); color: #a5b4fc; }
.badge-cyan { background: rgba(6,182,212,0.15); color: #67e8f9; }
.card-desc { font-size: 12px; color: #8b8fa3; margin-top: 4px; line-height: 1.55; }
.card-layman { font-size: 11px; color: #6b7280; margin-top: 3px; font-style: italic; }
.card-image { font-size: 11px; color: #6b7280; margin-top: 4px; font-family: 'SF Mono', monospace; }
.card-ports { font-size: 11px; color: #8b8fa3; margin-top: 6px; }
.card-ports span { display: inline-block; background: rgba(255,255,255,0.06); padding: 1px 6px; border-radius: 3px; margin: 1px 2px; font-family: 'SF Mono', monospace; }
.card-details { display: none; margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.06); font-size: 12px; line-height: 1.76; }
.card.expanded .card-details { display: block; }
.detail-row { display: flex; gap: 8px; padding: 4px 0; line-height: 1.7; min-height: 22px; border-bottom: 1px solid rgba(255,255,255,0.03); }
.detail-row:last-child { border-bottom: none; }
.detail-label { color: #6b7280; min-width: 90px; flex-shrink: 0; }
.detail-value { color: #c4c8e0; }
.dep-tag { display: inline-block; background: rgba(139,92,246,0.15); color: #c4b5fd; padding: 1px 6px; border-radius: 3px; margin: 1px 2px; font-size: 11px; cursor: pointer; transition: all 0.15s; }
.dep-tag:hover { background: rgba(139,92,246,0.3); }
/* Layer diagram */
.layer-stack { display: flex; flex-direction: column; gap: 0; margin: 20px 0; }
.layer { border: 1px solid rgba(255,255,255,0.08); padding: 16px 20px; position: relative; transition: all 0.2s; cursor: pointer; }
.layer:hover { background: rgba(255,255,255,0.04); }
.layer.expanded .layer-details { display: block; }
.layer:first-child { border-radius: 10px 10px 0 0; }
.layer:last-child { border-radius: 0 0 10px 10px; }
.layer-header { display: flex; justify-content: space-between; align-items: center; }
.layer-name { font-size: 14px; font-weight: 600; color: #fff; }
.layer-tag { font-size: 10px; padding: 2px 8px; border-radius: 4px; font-weight: 500; }
.layer-desc { font-size: 12px; color: #8b8fa3; margin-top: 4px; }
.layer-layman { font-size: 11px; color: #6b7280; margin-top: 2px; font-style: italic; }
.layer-details { display: none; margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255,255,255,0.06); font-size: 12px; line-height: 1.8; color: #c4c8e0; }
.layer-details code { background: rgba(255,255,255,0.06); padding: 1px 6px; border-radius: 3px; font-family: 'SF Mono', monospace; font-size: 11px; }
.layer-details h4 { color: #a5b4fc; font-size: 12px; font-weight: 600; margin: 8px 0 4px; text-transform: uppercase; letter-spacing: 0.5px; }
.layer-details ul { padding-left: 16px; margin: 4px 0; }
.layer-details li { margin: 2px 0; }
.l-hw { background: rgba(239,68,68,0.08); border-color: rgba(239,68,68,0.2); }
.l-hw .layer-name { color: #fca5a5; }
.l-os { background: rgba(245,158,11,0.08); border-color: rgba(245,158,11,0.2); }
.l-os .layer-name { color: #fcd34d; }
.l-enc { background: rgba(168,85,247,0.08); border-color: rgba(168,85,247,0.2); }
.l-enc .layer-name { color: #d8b4fe; }
.l-net { background: rgba(6,182,212,0.08); border-color: rgba(6,182,212,0.2); }
.l-net .layer-name { color: #67e8f9; }
.l-svc { background: rgba(99,102,241,0.08); border-color: rgba(99,102,241,0.2); }
.l-svc .layer-name { color: #a5b4fc; }
.l-cnt { background: rgba(16,185,129,0.08); border-color: rgba(16,185,129,0.2); }
.l-cnt .layer-name { color: #6ee7b7; }
.l-ui { background: rgba(236,72,153,0.08); border-color: rgba(236,72,153,0.2); }
.l-ui .layer-name { color: #f9a8d4; }
.l-kiosk { background: rgba(251,146,60,0.08); border-color: rgba(251,146,60,0.2); }
.l-kiosk .layer-name { color: #fdba74; }
/* Protocol cards */
.proto-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; margin-top: 16px; }
.proto-card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); border-radius: 10px; padding: 14px 16px; cursor: pointer; transition: all 0.2s; overflow-wrap: break-word; word-break: break-word; }
.proto-card:hover { background: rgba(255,255,255,0.06); border-color: rgba(255,255,255,0.12); }
.proto-card.expanded .proto-details { display: block; }
.proto-grid.all-expanded { align-items: stretch; }
.proto-grid.all-expanded .proto-card { display: flex; flex-direction: column; }
.proto-grid.all-expanded .proto-details { flex: 1; }
.proto-card-name { font-size: 14px; font-weight: 600; color: #fff; }
.proto-card-desc { font-size: 12px; color: #8b8fa3; margin-top: 4px; }
.proto-card-layman { font-size: 11px; color: #6b7280; margin-top: 2px; font-style: italic; }
.proto-details { display: none; margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.06); font-size: 12px; line-height: 1.6; color: #c4c8e0; }
/* Dependency graph */
.dep-graph { margin-top: 20px; }
.dep-graph h3 { font-size: 15px; font-weight: 600; margin-bottom: 12px; color: #fff; }
.dep-chain { font-family: 'SF Mono', Menlo, monospace; font-size: 13px; line-height: 1.8; color: #8b8fa3; background: rgba(255,255,255,0.02); border-radius: 10px; padding: 20px 24px; border: 1px solid rgba(255,255,255,0.04); white-space: pre; overflow-x: auto; }
.dep-chain .hl { color: #a5b4fc; font-weight: 600; cursor: pointer; }
.dep-chain .hl:hover { text-decoration: underline; }
.dep-chain .arrow { color: #4b5563; }
.dep-chain .comment { color: #4b5563; }
/* Data paths */
.path-tree { font-family: 'SF Mono', Menlo, monospace; font-size: 12px; line-height: 1.8; color: #8b8fa3; background: rgba(255,255,255,0.02); border-radius: 10px; padding: 20px 24px; border: 1px solid rgba(255,255,255,0.04); white-space: pre; overflow-x: auto; }
.path-tree .dir { color: #fcd34d; font-weight: 600; }
.path-tree .desc { color: #6b7280; }
/* Boot sequence */
.boot-step { display: flex; gap: 16px; align-items: flex-start; padding: 12px 0; border-bottom: 1px solid rgba(255,255,255,0.04); }
.boot-step:last-child { border-bottom: none; }
.boot-num { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; flex-shrink: 0; }
.boot-content { flex: 1; }
.boot-title { font-size: 13px; font-weight: 600; color: #fff; }
.boot-desc { font-size: 12px; color: #8b8fa3; margin-top: 2px; }
.boot-layman { font-size: 11px; color: #6b7280; margin-top: 2px; font-style: italic; }
.boot-detail { font-size: 11px; color: #6b7280; font-family: 'SF Mono', monospace; margin-top: 4px; }
/* Security section */
.sec-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 10px; margin-top: 16px; }
.sec-card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); border-radius: 10px; padding: 14px 16px; }
.sec-title { font-size: 13px; font-weight: 600; color: #fff; }
.sec-desc { font-size: 12px; color: #8b8fa3; margin-top: 4px; line-height: 1.65; }
.sec-layman { font-size: 11px; color: #6b7280; margin-top: 2px; font-style: italic; }
/* Legend */
.legend { display: flex; gap: 20px; flex-wrap: wrap; font-size: 12px; color: #6b7280; margin-top: 20px; padding-top: 16px; border-top: 1px solid rgba(255,255,255,0.04); }
.legend-item { display: flex; align-items: center; gap: 6px; }
.legend-dot { width: 10px; height: 10px; border-radius: 3px; }
.hidden { display: none !important; }
/* Glossary */
.glossary { column-count: 2; column-gap: 24px; margin-top: 16px; }
@media (max-width: 700px) { .glossary { column-count: 1; } }
.glossary-item { break-inside: avoid; margin-bottom: 12px; padding: 10px 14px; background: rgba(255,255,255,0.02); border-radius: 8px; border: 1px solid rgba(255,255,255,0.04); }
.glossary-term { font-size: 13px; font-weight: 600; color: #fff; }
.glossary-tech { font-size: 12px; color: #a5b4fc; margin-top: 2px; }
.glossary-plain { font-size: 12px; color: #8b8fa3; margin-top: 2px; }
/* Popovers */
.kw { border-bottom: 1px dotted rgba(165,180,252,0.5); color: #c4b5fd; cursor: help; }
.kw:hover { color: #a5b4fc; border-bottom-color: rgba(165,180,252,0.8); }
.popover-overlay { position: fixed; inset: 0; z-index: 9998; }
.popover { position: fixed; z-index: 9999; background: #16133a; border: 1px solid rgba(99,102,241,0.35); border-radius: 10px; padding: 16px 18px 14px; width: 360px; max-width: 90vw; max-height: 80vh; overflow-y: auto; box-shadow: 0 12px 48px rgba(0,0,0,0.6); animation: popIn 0.15s ease-out; }
@keyframes popIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
.pop-title { font-size: 15px; font-weight: 700; color: #fff; margin-bottom: 10px; padding-right: 24px; }
.pop-section { margin-bottom: 10px; }
.pop-section:last-child { margin-bottom: 0; }
.pop-label { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.2px; margin-bottom: 3px; }
.pop-label-tech { color: #818cf8; }
.pop-label-plain { color: #34d399; }
.pop-text { font-size: 12px; line-height: 1.65; color: #c4c8e0; }
.pop-text-plain { color: #a0a4b8; font-style: italic; }
.pop-close { position: absolute; top: 10px; right: 12px; background: none; border: none; color: #6b7280; cursor: pointer; font-size: 18px; line-height: 1; padding: 2px 6px; border-radius: 4px; }
.pop-close:hover { color: #fff; background: rgba(255,255,255,0.1); }
.pop-used { font-size: 11px; color: #6b7280; margin-top: 6px; }
.pop-used code { background: rgba(255,255,255,0.06); padding: 1px 5px; border-radius: 3px; font-family: 'SF Mono', monospace; font-size: 10px; color: #8b8fa3; }
/* ═══ System selector ═══ */
.sys-selector { display: flex; gap: 0; background: #0d1120; border-bottom: 1px solid rgba(255,255,255,0.06); padding: 0 32px; }
.sys-btn { padding: 10px 20px; border: none; background: transparent; color: #6b7280; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s; border-bottom: 3px solid transparent; }
.sys-btn:hover { color: #c4c8e0; background: rgba(255,255,255,0.02); }
.sys-btn.active[data-system="archy"] { color: #a5b4fc; border-bottom-color: #6366f1; background: rgba(99,102,241,0.05); }
.sys-btn.active[data-system="start9"] { color: #6ee7b7; border-bottom-color: #10b981; background: rgba(16,185,129,0.05); }
.sys-btn.active[data-system="umbrelos"] { color: #c4b5fd; border-bottom-color: #8b5cf6; background: rgba(139,92,246,0.05); }
.sys-btn.active[data-system="comparison"] { color: #fcd34d; border-bottom-color: #f59e0b; background: rgba(245,158,11,0.05); }
.sys-version { font-size: 11px; color: #4b5563; font-weight: 400; margin-left: 6px; }
/* ═══ Comparison tables ═══ */
.cmp-table { width: 100%; border-collapse: collapse; font-size: 12px; margin-top: 16px; }
.cmp-table th { text-align: left; padding: 8px 12px; background: rgba(255,255,255,0.04); color: #a5b4fc; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid rgba(255,255,255,0.08); }
.cmp-table td { padding: 8px 12px; border-bottom: 1px solid rgba(255,255,255,0.04); color: #c4c8e0; vertical-align: top; }
.cmp-table tr:hover td { background: rgba(255,255,255,0.02); }
.cmp-label { color: #8b8fa3; font-weight: 500; min-width: 140px; }
.compare-row-label { padding: 10px 16px; background: rgba(99,102,241,0.08); font-size: 12px; font-weight: 600; color: #a5b4fc; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid rgba(255,255,255,0.06); border-top: 1px solid rgba(99,102,241,0.15); border-left: 3px solid rgba(99,102,241,0.4); }
.compare-cell.good { color: #6ee7b7; }
.compare-cell.warn { color: #fcd34d; }
.compare-cell.bad { color: #fca5a5; }
.compare-cell.notes { color: #9ca3af; font-style: italic; }
.sys-tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 600; letter-spacing: 0.3px; }
.sys-archy { background: rgba(99,102,241,0.15); color: #a5b4fc; }
.sys-start9 { background: rgba(16,185,129,0.15); color: #6ee7b7; }
.sys-umbrel { background: rgba(139,92,246,0.15); color: #c4b5fd; }
/* ═══ Structural additions ═══ */
.section-header { margin-bottom: 16px; }
.section-header h2 { font-size: 20px; font-weight: 700; margin-bottom: 4px; }
.section-header p { font-size: 13px; color: #8b8fa3; }
.section-header code { background: rgba(255,255,255,0.06); padding: 1px 6px; border-radius: 3px; font-family: 'SF Mono', monospace; font-size: 11px; }
.subsection-title { font-size: 15px; font-weight: 600; margin: 28px 0 10px; color: #fff; padding-left: 12px; border-left: 3px solid rgba(99,102,241,0.5); }
.section > .stats + .layer-stack { border-top: 1px solid rgba(255,255,255,0.04); padding-top: 16px; }
.cmp-table { border: 1px solid rgba(255,255,255,0.06); border-radius: 8px; overflow: hidden; }
.sec-desc code, .proto-details code, .boot-desc code { background: rgba(255,255,255,0.06); padding: 1px 5px; border-radius: 3px; font-family: 'SF Mono', monospace; font-size: 11px; }
.not-implemented { text-align: center; padding: 60px 20px; color: #4b5563; }
.not-implemented h3 { font-size: 16px; color: #6b7280; margin-bottom: 8px; }
.not-implemented p { font-size: 13px; }
/* ═══ Header controls (light mode + large text) ═══ */
.header-controls { position: absolute; top: 28px; right: 32px; display: flex; gap: 6px; }
.toggle-btn { background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; padding: 4px 10px; color: #6b7280; cursor: pointer; font-size: 11px; font-weight: 500; transition: all 0.2s; font-family: inherit; }
.toggle-btn:hover { background: rgba(255,255,255,0.1); color: #c4c8e0; }
.toggle-btn.active { background: rgba(99,102,241,0.15); border-color: rgba(99,102,241,0.3); color: #a5b4fc; }
/* ═══ Large text mode ═══ */
body.large-text { font-size: 15px; }
body.large-text .card-name, body.large-text .sec-title, body.large-text .boot-title, body.large-text .proto-card-name, body.large-text .layer-name, body.large-text .glossary-term { font-size: 16px; }
body.large-text .card-desc, body.large-text .card-layman, body.large-text .layer-desc, body.large-text .layer-layman,
body.large-text .sec-desc, body.large-text .sec-layman, body.large-text .boot-desc, body.large-text .boot-layman,
body.large-text .proto-card-desc, body.large-text .proto-card-layman, body.large-text .glossary-tech, body.large-text .glossary-plain,
body.large-text .cmp-table td, body.large-text .card-details, body.large-text .layer-details,
body.large-text .proto-details, body.large-text .detail-label, body.large-text .detail-value,
body.large-text .pop-text, body.large-text .pop-text-plain { font-size: 14px; }
body.large-text .nav-btn, body.large-text .sys-btn { font-size: 15px; }
body.large-text .stat-label, body.large-text .card-image, body.large-text .card-ports, body.large-text .boot-detail,
body.large-text .dep-tag, body.large-text .dep-chain, body.large-text .path-tree, body.large-text .filter-btn,
body.large-text .cmp-table th, body.large-text .compare-row-label { font-size: 13px; }
body.large-text .header p, body.large-text .section-header p { font-size: 14px; }
/* ═══ LIGHT MODE ═══ */
body.light { background: #fafafa; color: #0a0a0a; }
body.light .header { background: linear-gradient(135deg, #f0f0f8 0%, #e8e0f0 100%); border-bottom-color: rgba(99,102,241,0.15); }
body.light .header h1 { color: #1a1a2e; }
body.light .header p { color: #666; }
body.light .sys-selector { background: #f0f0f5; border-bottom-color: rgba(0,0,0,0.06); }
body.light .sys-btn { color: #999; }
body.light .sys-btn:hover { color: #333; background: rgba(0,0,0,0.03); }
body.light .sys-btn.active[data-system="archy"] { color: #4f46e5; border-bottom-color: #4f46e5; background: rgba(79,70,229,0.05); }
body.light .sys-btn.active[data-system="start9"] { color: #059669; border-bottom-color: #059669; background: rgba(5,150,105,0.05); }
body.light .sys-btn.active[data-system="umbrelos"] { color: #7c3aed; border-bottom-color: #7c3aed; background: rgba(124,58,237,0.05); }
body.light .sys-btn.active[data-system="comparison"] { color: #b45309; border-bottom-color: #b45309; background: rgba(180,83,9,0.05); }
body.light .nav { background: #eaeaef; border-bottom-color: rgba(0,0,0,0.06); }
body.light .nav-btn { color: #888; }
body.light .nav-btn:hover { color: #333; }
body.light .nav-btn.active { color: #4f46e5; border-bottom-color: #4f46e5; background: rgba(79,70,229,0.05); }
body.light .stat { background: #fff; border-color: rgba(0,0,0,0.06); }
body.light .stat-value { color: #4f46e5; }
body.light .stat-label { color: #888; }
body.light .layer { border-color: rgba(0,0,0,0.08); }
body.light .layer:hover { background: rgba(0,0,0,0.02); }
body.light .layer-name, body.light .card-name, body.light .sec-title, body.light .boot-title,
body.light .proto-card-name, body.light .glossary-term, body.light .dep-graph h3,
body.light .subsection-title, body.light .pop-title, body.light .section-header h2 { color: #1a1a2e; }
body.light .layer-desc, body.light .card-desc, body.light .sec-desc, body.light .boot-desc,
body.light .proto-card-desc, body.light .detail-value, body.light .layer-details,
body.light .proto-details, body.light .pop-text, body.light .cmp-table td { color: #555; }
body.light .layer-layman, body.light .card-layman, body.light .sec-layman, body.light .boot-layman,
body.light .proto-card-layman, body.light .detail-label, body.light .glossary-plain,
body.light .boot-detail, body.light .pop-text-plain, body.light .cmp-label,
body.light .section-header p { color: #888; }
body.light .layer-details code { background: rgba(0,0,0,0.05); color: #4f46e5; }
body.light .layer-details h4, body.light .glossary-tech { color: #4f46e5; }
body.light .card, body.light .proto-card, body.light .sec-card, body.light .glossary-item { background: #fff; border-color: rgba(0,0,0,0.06); }
body.light .card:hover, body.light .proto-card:hover { background: #f8f8fc; border-color: rgba(0,0,0,0.1); }
body.light .card.expanded { border-color: rgba(79,70,229,0.3); }
body.light .card-details, body.light .proto-details, body.light .boot-step { border-color: rgba(0,0,0,0.06); }
body.light .dep-chain, body.light .path-tree { background: #f5f5f8; border-color: rgba(0,0,0,0.06); color: #888; }
body.light .dep-chain .hl, body.light .path-tree .dir { color: #4f46e5; }
body.light .dep-chain .arrow, body.light .dep-chain .comment { color: #ccc; }
body.light .cmp-table th { background: #f0f0f5; color: #4f46e5; border-bottom-color: rgba(0,0,0,0.06); }
body.light .cmp-label { color: #333; }
body.light .compare-row-label { background: #f0f0f5; color: #4f46e5; border-color: rgba(0,0,0,0.04); }
body.light .compare-cell.good { color: #059669; }
body.light .compare-cell.warn { color: #b45309; }
body.light .compare-cell.bad { color: #dc2626; }
body.light .compare-cell.notes { color: #666; }
body.light .filter-btn { border-color: rgba(0,0,0,0.08); color: #888; }
body.light .filter-btn.active { background: rgba(79,70,229,0.1); border-color: rgba(79,70,229,0.3); color: #4f46e5; }
body.light .kw { color: #7c3aed; border-bottom-color: rgba(124,58,237,0.3); }
body.light .popover { background: #fff; border-color: rgba(0,0,0,0.1); box-shadow: 0 12px 48px rgba(0,0,0,0.12); }
body.light .pop-close { color: #999; }
body.light .pop-close:hover { color: #333; background: rgba(0,0,0,0.05); }
body.light .toggle-btn { background: rgba(0,0,0,0.04); border-color: rgba(0,0,0,0.08); color: #888; }
body.light .toggle-btn:hover { background: rgba(0,0,0,0.06); color: #333; }
body.light .toggle-btn.active { background: rgba(79,70,229,0.1); border-color: rgba(79,70,229,0.2); color: #4f46e5; }
body.light .dep-tag { background: rgba(79,70,229,0.1); color: #4f46e5; }
body.light .tier-label::after, body.light .legend { border-color: rgba(0,0,0,0.06); }
body.light .sys-version { color: #aaa; }
body.light .l-hw { background: rgba(239,68,68,0.05); border-color: rgba(239,68,68,0.15); }
body.light .l-os { background: rgba(245,158,11,0.05); border-color: rgba(245,158,11,0.15); }
body.light .l-enc { background: rgba(168,85,247,0.05); border-color: rgba(168,85,247,0.15); }
body.light .l-net { background: rgba(6,182,212,0.05); border-color: rgba(6,182,212,0.15); }
body.light .l-svc { background: rgba(99,102,241,0.05); border-color: rgba(99,102,241,0.15); }
body.light .l-cnt { background: rgba(16,185,129,0.05); border-color: rgba(16,185,129,0.15); }
body.light .l-ui { background: rgba(236,72,153,0.05); border-color: rgba(236,72,153,0.15); }
body.light .l-kiosk { background: rgba(251,146,60,0.05); border-color: rgba(251,146,60,0.15); }
/* ═══ MOBILE ═══ */
@media (max-width: 768px) {
/* Header: compact, hide subtitle, move controls */
.header { padding: 12px 16px; }
.header h1 { font-size: 18px; }
.header p { display: none; }
.header-controls { position: static; display: flex; margin-top: 8px; }
/* System selector: scroll horizontally, smaller text */
.sys-selector { padding: 0 8px; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
.sys-selector::-webkit-scrollbar { display: none; }
.sys-btn { padding: 8px 14px; font-size: 13px; white-space: nowrap; flex-shrink: 0; }
.sys-version { display: none; }
/* Nav: scroll horizontally, smaller */
.nav { padding: 0; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
.nav::-webkit-scrollbar { display: none; }
.nav-btn { padding: 10px 14px; font-size: 12px; flex-shrink: 0; }
/* Section content: reduce padding */
.section { padding: 16px; }
/* Stats: 2 columns on mobile */
.stats { grid-template-columns: repeat(3, 1fr); gap: 8px; }
.stat { padding: 10px 8px; }
.stat-value { font-size: 18px; }
.stat-label { font-size: 10px; }
/* Cards: single column */
.cards { grid-template-columns: 1fr; }
.proto-grid { grid-template-columns: 1fr; }
.sec-grid { grid-template-columns: 1fr; }
/* Layers: tighter padding */
.layer { padding: 12px 14px; }
/* Dep chain: smaller font */
.dep-chain { font-size: 11px; padding: 12px 14px; overflow-x: auto; }
/* Path tree: smaller font */
.path-tree { font-size: 10px; padding: 12px 14px; }
/* Comparison table: horizontal scroll */
.cmp-table { display: block; overflow-x: auto; }
/* Glossary: single column */
.glossary { column-count: 1; }
/* Resource grid: single column */
.section > div[style*="grid-template-columns: 1fr 1fr"] { display: flex !important; flex-direction: column !important; }
/* Popover: full width on mobile */
.popover { width: calc(100vw - 32px); left: 16px !important; }
/* Boot steps: tighter */
.boot-step { gap: 12px; }
.boot-num { width: 24px; height: 24px; font-size: 11px; }
/* Toggle buttons */
.toggle-btn { padding: 6px 10px; font-size: 11px; }
}
/* Small phones */
@media (max-width: 400px) {
.stats { grid-template-columns: repeat(2, 1fr); }
.sys-btn { padding: 8px 10px; font-size: 12px; }
.nav-btn { padding: 8px 10px; font-size: 11px; }
.header h1 { font-size: 16px; }
}
</style>
<body>
<div class="sticky-top">
<div class="header">
<h1>Archipelago System Architecture</h1>
<p>Complete interactive map of every layer, protocol, container, and data path &middot; Click anything to expand</p>
<div class="header-controls">
<button class="toggle-btn" onclick="document.body.classList.toggle('light'); this.classList.toggle('active')">Light</button>
<button class="toggle-btn" onclick="document.body.classList.toggle('large-text'); this.classList.toggle('active')">Larger</button>
</div>
</div>
<div class="sys-selector">
<button class="sys-btn active" data-system="archy">Archipelago<span class="sys-version">v0.1.0</span></button>
<button class="sys-btn" data-system="start9">StartOS<span class="sys-version">v0.4.0-beta</span></button>
<button class="sys-btn" data-system="umbrelos">umbrelOS<span class="sys-version">v1.6.1</span></button>
<button class="sys-btn" data-system="comparison">Comparison</button>
</div>
<div class="nav">
<button class="nav-btn active" data-section="overview">Overview</button>
<button class="nav-btn" data-section="layers">System Layers</button>
<button class="nav-btn" data-section="containers">Containers</button>
<button class="nav-btn" data-section="protocols">Protocols</button>
<button class="nav-btn" data-section="web5">Web5</button>
<button class="nav-btn" data-section="boot">Boot Sequence</button>
<button class="nav-btn" data-section="security">Security</button>
<button class="nav-btn" data-section="data">Data Paths</button>
<button class="nav-btn" data-section="glossary">Glossary</button>
</div>
</div><!-- end sticky-top -->
<!-- ============================================================ -->
<!-- OVERVIEW -->
<!-- ============================================================ -->
<div class="section active" id="sec-overview">
<div class="stats">
<div class="stat"><div class="stat-value">34</div><div class="stat-label">Containers</div></div>
<div class="stat"><div class="stat-value">260+</div><div class="stat-label">RPC Methods</div></div>
<div class="stat"><div class="stat-value">9</div><div class="stat-label">Protocols</div></div>
<div class="stat"><div class="stat-value">LUKS2</div><div class="stat-label">Encryption</div></div>
<div class="stat"><div class="stat-value">Rootless</div><div class="stat-label">Podman</div></div>
<div class="stat"><div class="stat-value">8 GB+</div><div class="stat-label">Recommended RAM</div></div>
</div>
<div class="layer-stack">
<div class="layer l-kiosk" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Kiosk Display</span>
<span class="layer-tag badge-amber">Layer 8 &middot; Physical</span>
</div>
<div class="layer-desc">X11 + Chromium fullscreen on VT7, showing the web UI directly on connected monitor</div>
<div class="layer-layman">The TV/monitor screen you see when the box is plugged in. No keyboard needed &mdash; it just shows the dashboard.</div>
</div>
<div class="layer l-ui" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Web UI (Vue.js SPA)</span>
<span class="layer-tag badge-purple">Layer 7 &middot; Application</span>
</div>
<div class="layer-desc">Vue 3 + TypeScript + Pinia frontend served by nginx, communicates via JSON-RPC and WebSocket</div>
<div class="layer-layman">The dashboard you use in your browser to manage everything &mdash; apps, Bitcoin, settings.</div>
</div>
<div class="layer l-svc" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Rust Backend</span>
<span class="layer-tag badge-blue">Layer 6 &middot; Service</span>
</div>
<div class="layer-desc">Archipelago binary on 127.0.0.1:5678 &mdash; RPC server, auth, session management, container orchestration, Tor control</div>
<div class="layer-layman">The brain of the system. Handles login, manages containers, talks to Bitcoin, and coordinates everything.</div>
</div>
<div class="layer l-cnt" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Container Layer (Podman Rootless)</span>
<span class="layer-tag badge-green">Layer 5 &middot; Isolation</span>
</div>
<div class="layer-desc">34 containers on archy-net (internal DNS) and bridge networks, managed by rootless Podman</div>
<div class="layer-layman">Each app runs in its own sandbox. If one app crashes or gets hacked, the others are unaffected.</div>
</div>
<div class="layer l-net" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Network Layer</span>
<span class="layer-tag badge-cyan">Layer 4 &middot; Network</span>
</div>
<div class="layer-desc">Nginx reverse proxy (80/443), Tailscale mesh VPN, Tor hidden services, UFW firewall</div>
<div class="layer-layman">Controls what traffic goes where. One front door (nginx) routes requests to the right app. Tor makes you reachable without exposing your IP.</div>
</div>
<div class="layer l-enc" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Encryption Layer</span>
<span class="layer-tag badge-purple">Layer 3 &middot; Security</span>
</div>
<div class="layer-desc">LUKS2 full-disk encryption on /var/lib/archipelago with auto-detected cipher (AES-XTS or ChaCha20-Adiantum)</div>
<div class="layer-layman">All your Bitcoin data, passwords, and app data are encrypted. If someone steals the hard drive, they get nothing.</div>
</div>
<div class="layer l-os" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Operating System</span>
<span class="layer-tag badge-amber">Layer 2 &middot; OS</span>
</div>
<div class="layer-desc">Debian 12 (Bookworm) minimal &mdash; systemd services, x86_64/ARM64, debootstrap custom base</div>
<div class="layer-layman">The operating system. Debian is rock-solid Linux used by servers worldwide. We strip it down to just what's needed.</div>
</div>
<div class="layer l-hw" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Hardware / Boot</span>
<span class="layer-tag badge-red">Layer 1 &middot; Physical</span>
</div>
<div class="layer-desc">UEFI + BIOS dual-boot, GPT partitions, USB flash installer, auto-detect disk + network + CPU features</div>
<div class="layer-layman">The physical computer. Flash a USB stick, boot from it, and the installer sets everything up automatically.</div>
</div>
</div>
<div class="dep-graph">
<h3>Container Dependency Chain</h3>
<div class="dep-chain"><span class="comment">// Startup order: Databases &rarr; Core &rarr; Services &rarr; Apps</span>
<span class="comment">// Health monitor restarts in this order too</span>
<span class="hl" onclick="showContainers('archy-mempool-db')">mempool-db</span> <span class="arrow">───┐</span>
<span class="hl" onclick="showContainers('archy-btcpay-db')">btcpay-db</span> <span class="arrow">───┤</span>
<span class="arrow"></span>
<span class="arrow">├──→</span> <span class="hl" onclick="showContainers('bitcoin-knots')">bitcoin-knots</span> <span class="arrow">──→</span> <span class="hl" onclick="showContainers('electrumx')">electrumx</span>
<span class="arrow"></span> <span class="arrow"></span>
<span class="arrow"></span> <span class="arrow">┌────┴────┬──────────┬──────────┐</span>
<span class="arrow"></span> <span class="arrow"></span> <span class="arrow"></span> <span class="arrow"></span> <span class="arrow"></span>
<span class="arrow"></span> <span class="hl" onclick="showContainers('lnd')">lnd</span> <span class="hl" onclick="showContainers('fedimint')">fedimint</span> <span class="hl" onclick="showContainers('mempool-api')">mempool-api</span> <span class="hl" onclick="showContainers('archy-nbxplorer')">nbxplorer</span>
<span class="arrow"></span> <span class="arrow"></span> <span class="arrow"></span> <span class="arrow"></span> <span class="arrow"></span>
<span class="arrow"></span> <span class="arrow"></span> <span class="hl" onclick="showContainers('fedimint-gateway')">fedi-gw</span> <span class="hl" onclick="showContainers('archy-mempool-web')">mempool-web</span> <span class="hl" onclick="showContainers('btcpay-server')">btcpay</span>
<span class="arrow"></span> <span class="arrow"></span>
<span class="arrow"></span> <span class="arrow">└──→</span> <span class="hl" onclick="showContainers('archy-lnd-ui')">lnd-ui</span>
<span class="comment">// IndeedHub stack (independent)</span>
<span class="hl" onclick="showContainers('indeedhub-postgres')">ih-postgres</span> <span class="arrow">──→</span> <span class="hl" onclick="showContainers('indeedhub-api')">ih-api</span> <span class="arrow">──→</span> <span class="hl" onclick="showContainers('indeedhub')">indeedhub</span>
<span class="hl" onclick="showContainers('indeedhub-redis')">ih-redis</span> <span class="arrow">──→</span> <span class="hl" onclick="showContainers('indeedhub-api')">ih-api</span>
<span class="hl" onclick="showContainers('indeedhub-minio')">ih-minio</span> <span class="arrow">──→</span> <span class="hl" onclick="showContainers('indeedhub-api')">ih-api</span>
<span class="comment">// Penpot stack (independent)</span>
<span class="hl" onclick="showContainers('penpot-postgres')">penpot-pg</span> <span class="arrow">──→</span> <span class="hl" onclick="showContainers('penpot-backend')">penpot-be</span> <span class="arrow">──→</span> <span class="hl" onclick="showContainers('penpot-frontend')">penpot-fe</span>
<span class="hl" onclick="showContainers('penpot-valkey')">penpot-vk</span> <span class="arrow">──→</span> <span class="hl" onclick="showContainers('penpot-backend')">penpot-be</span> <span class="arrow">──→</span> <span class="hl" onclick="showContainers('penpot-exporter')">penpot-exp</span>
<span class="comment">// Tier 3: All independent &mdash; start in any order</span>
<span class="hl" onclick="showContainers('filebrowser')">filebrowser</span> <span class="hl" onclick="showContainers('grafana')">grafana</span> <span class="hl" onclick="showContainers('homeassistant')">homeassist</span> <span class="hl" onclick="showContainers('jellyfin')">jellyfin</span> <span class="hl" onclick="showContainers('photoprism')">photoprism</span>
<span class="hl" onclick="showContainers('vaultwarden')">vaultwarden</span> <span class="hl" onclick="showContainers('nextcloud')">nextcloud</span> <span class="hl" onclick="showContainers('searxng')">searxng</span> <span class="hl" onclick="showContainers('uptime-kuma')">uptime-kuma</span> <span class="hl" onclick="showContainers('ollama')">ollama</span>
<span class="hl" onclick="showContainers('onlyoffice')">onlyoffice</span> <span class="hl" onclick="showContainers('nginx-proxy-manager')">nginx-pm</span> <span class="hl" onclick="showContainers('portainer')">portainer</span></div>
</div>
<h3 class="subsection-title">System Resources</h3>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px;">
<div style="background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.06); border-radius:10px; padding:16px;">
<div style="font-size:13px; font-weight:600; color:#fff; margin-bottom:10px; border-bottom:1px solid rgba(255,255,255,0.06); padding-bottom:8px;">Hardware Requirements</div>
<table class="cmp-table" style="margin:0;">
<tr><td class="cmp-label">Minimum RAM</td><td>4 GB</td></tr>
<tr><td class="cmp-label">Recommended RAM</td><td>8 GB+ (core stack uses ~8&ndash;10 GB)</td></tr>
<tr><td class="cmp-label">Minimum Disk</td><td>32 GB SSD</td></tr>
<tr><td class="cmp-label">Recommended Disk</td><td>1 TB+ NVMe SSD</td></tr>
<tr><td class="cmp-label">CPU</td><td>x86_64 or ARM64, 4+ cores recommended</td></tr>
<tr><td class="cmp-label">Network</td><td>Ethernet recommended (WiFi supported)</td></tr>
<tr><td class="cmp-label">Targets</td><td>HP ProDesk, Intel NUC, any standard PC</td></tr>
</table>
</div>
<div style="background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.06); border-radius:10px; padding:16px;">
<div style="font-size:13px; font-weight:600; color:#fff; margin-bottom:10px; border-bottom:1px solid rgba(255,255,255,0.06); padding-bottom:8px;">Memory Budget (all containers)</div>
<table class="cmp-table" style="margin:0;">
<tr><td class="cmp-label">Bitcoin Knots</td><td>2 GB (1 GB low-memory mode)</td></tr>
<tr><td class="cmp-label">ElectrumX</td><td>1 GB</td></tr>
<tr><td class="cmp-label">LND</td><td>512 MB</td></tr>
<tr><td class="cmp-label">BTCPay + DB</td><td>1.5 GB (1 GB + 512 MB)</td></tr>
<tr><td class="cmp-label">Mempool stack</td><td>1.3 GB (512+256+512 MB)</td></tr>
<tr><td class="cmp-label">Fedimint + GW</td><td>1 GB (512+512 MB)</td></tr>
<tr><td class="cmp-label">Ollama (AI)</td><td>4 GB (1 GB low-memory)</td></tr>
<tr><td class="cmp-label">All other apps</td><td>128&ndash;1024 MB each</td></tr>
<tr><td class="cmp-label" style="color:#fff;"><b>Total allocated</b></td><td style="color:#fff;"><b>~20 GB</b> (not all run simultaneously)</td></tr>
</table>
</div>
</div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px;">
<div style="background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.06); border-radius:10px; padding:16px;">
<div style="font-size:13px; font-weight:600; color:#fff; margin-bottom:10px; border-bottom:1px solid rgba(255,255,255,0.06); padding-bottom:8px;">Disk Usage by Component</div>
<table class="cmp-table" style="margin:0;">
<tr><td class="cmp-label">Bitcoin blockchain (full)</td><td>~600 GB</td></tr>
<tr><td class="cmp-label">Bitcoin (pruned)</td><td>~550 MB</td></tr>
<tr><td class="cmp-label">ElectrumX index</td><td>~50 GB</td></tr>
<tr><td class="cmp-label">LND channels + wallet</td><td>~1 GB</td></tr>
<tr><td class="cmp-label">Databases (all)</td><td>~2&ndash;10 GB</td></tr>
<tr><td class="cmp-label">Container images</td><td>~15 GB</td></tr>
<tr><td class="cmp-label">Ollama models</td><td>1&ndash;50 GB (varies)</td></tr>
<tr><td class="cmp-label">Media (Jellyfin/Photos)</td><td>User-determined</td></tr>
</table>
</div>
<div style="background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.06); border-radius:10px; padding:16px;">
<div style="font-size:13px; font-weight:600; color:#fff; margin-bottom:10px; border-bottom:1px solid rgba(255,255,255,0.06); padding-bottom:8px;">Network Ports (External)</div>
<table class="cmp-table" style="margin:0;">
<tr><td class="cmp-label">80 / 443</td><td>Nginx &rarr; Web UI, app proxies</td></tr>
<tr><td class="cmp-label">8333</td><td>Bitcoin P2P (node discovery)</td></tr>
<tr><td class="cmp-label">9735</td><td>Lightning P2P (payment routing)</td></tr>
<tr><td class="cmp-label">50001</td><td>Electrum protocol (wallet queries)</td></tr>
<tr><td class="cmp-label">22</td><td>SSH (admin access)</td></tr>
<tr style="border-top:1px solid rgba(255,255,255,0.06);"><td class="cmp-label" style="color:#6b7280; font-style:italic;">Internal only</td><td style="color:#6b7280; font-style:italic;">8332 (RPC), 10009 (gRPC), 8080 (REST), 8999, 4080, 3000, 3001, 8082&ndash;8096, 9000&hellip;</td></tr>
</table>
</div>
</div>
<div style="display:grid; grid-template-columns: 1fr; gap: 12px; margin-top: 12px;">
<div style="background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.06); border-radius:10px; padding:16px;">
<div style="font-size:13px; font-weight:600; color:#fff; margin-bottom:10px; border-bottom:1px solid rgba(255,255,255,0.06); padding-bottom:8px;">Container Security Defaults</div>
<table class="cmp-table" style="margin:0;">
<tr><td class="cmp-label">Capabilities</td><td><code>--cap-drop=ALL</code> then add only needed: CHOWN, FOWNER, SETUID, SETGID, DAC_OVERRIDE. Some get NET_RAW (LND), NET_BIND_SERVICE (Vaultwarden, nginx-pm, LND-UI).</td></tr>
<tr><td class="cmp-label">Privileges</td><td><code>--security-opt=no-new-privileges</code> on all containers</td></tr>
<tr><td class="cmp-label">Health checks</td><td>All containers: <code>--health-interval=120s --health-timeout=5s --health-retries=3</code></td></tr>
<tr><td class="cmp-label">Low-memory mode</td><td>Auto-detected: Bitcoin 2G&rarr;1G, PhotoPrism 1G&rarr;512M, OnlyOffice 2G&rarr;1G, Ollama 4G&rarr;1G</td></tr>
<tr><td class="cmp-label">Disk mode</td><td>Auto: if disk &lt;1TB &rarr; Bitcoin prune=550, dbcache=512M. If &ge;1TB &rarr; full txindex, dbcache=4G</td></tr>
<tr><td class="cmp-label">RPC methods</td><td>260+ registered across 20+ namespaces (auth, seed, package, bitcoin, lnd, identity, tor, nostr, mesh, federation, dwn, system, monitoring&hellip;)</td></tr>
</table>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- SYSTEM LAYERS (detailed) -->
<!-- ============================================================ -->
<div class="section" id="sec-layers">
<div class="layer-stack">
<div class="layer l-hw" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">1. Hardware / Boot</span>
<span class="layer-tag badge-red">Physical</span>
</div>
<div class="layer-desc">UEFI + BIOS dual-boot installer, GPT partition table, auto-detect hardware</div>
<div class="layer-layman">You flash a USB drive, plug it in, and the computer installs itself. Works on both old and new machines.</div>
<div class="layer-details">
<h4>Partition Layout</h4>
<ul>
<li><code>1 MB</code> &mdash; BIOS boot (for older machines without UEFI)</li>
<li><code>512 MB</code> &mdash; EFI System Partition (UEFI boot files)</li>
<li><code>30 GB</code> &mdash; Root filesystem (Debian OS, binaries, Podman storage)</li>
<li><code>Remaining</code> &mdash; LUKS2 encrypted &rarr; <code>/var/lib/archipelago</code> (all user data)</li>
</ul>
<h4>Installer Features</h4>
<ul>
<li>Auto-detects target disk (largest available, prefers NVMe)</li>
<li>Auto-detects AES-NI CPU support for encryption cipher selection</li>
<li>Debootstrap minimal Debian 12 (no bloat)</li>
<li>Configures GRUB for both UEFI and legacy BIOS</li>
<li>Creates <code>archipelago</code> user (UID 1000) with Podman subuid/subgid mapping</li>
</ul>
<h4>Hardware Targets</h4>
<ul>
<li><b>x86_64:</b> HP ProDesk, Intel NUC, any standard PC</li>
<li><b>ARM64:</b> Planned but not primary target yet</li>
<li><b>Minimum:</b> 4 cores, 8GB RAM, 256GB disk</li>
<li><b>Recommended:</b> 4+ cores, 16GB RAM, 1TB+ disk (for full Bitcoin node)</li>
</ul>
</div>
</div>
<div class="layer l-os" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">2. Operating System &mdash; Debian 12</span>
<span class="layer-tag badge-amber">OS</span>
</div>
<div class="layer-desc">Minimal Debian Bookworm with systemd, custom kernel parameters, hardened services</div>
<div class="layer-layman">The foundation. Debian is one of the most stable and trusted Linux versions. We remove everything unnecessary.</div>
<div class="layer-details">
<h4>Key Packages</h4>
<ul>
<li><code>podman</code> &mdash; Rootless container runtime (replaces Docker)</li>
<li><code>nginx</code> &mdash; Reverse proxy (front door for all web traffic)</li>
<li><code>tor</code> &mdash; Privacy network daemon</li>
<li><code>tailscale</code> &mdash; Mesh VPN for remote access</li>
<li><code>chromium</code> &mdash; Kiosk browser for local display</li>
<li><code>cryptsetup</code> &mdash; LUKS disk encryption</li>
<li><code>xorg</code> &mdash; Display server for kiosk mode</li>
</ul>
<h4>Kernel Tuning</h4>
<ul>
<li><code>net.ipv4.ip_unprivileged_port_start=80</code> &mdash; lets rootless Podman bind ports 80+</li>
<li><code>vm.overcommit_memory=1</code> &mdash; for Redis/Valkey container requirements</li>
<li>User namespaces enabled for rootless containers</li>
</ul>
<h4>Users</h4>
<ul>
<li><code>archipelago</code> (UID 1000) &mdash; main user, owns all containers and data</li>
<li><code>root</code> &mdash; only for Tor management, LUKS, and boot services</li>
</ul>
</div>
</div>
<div class="layer l-enc" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">3. Encryption &mdash; LUKS2</span>
<span class="layer-tag badge-purple">Security</span>
</div>
<div class="layer-desc">Full-disk encryption on all user data with auto-detected hardware-accelerated ciphers</div>
<div class="layer-layman">Your Bitcoin wallet, passwords, photos &mdash; everything is scrambled. Without the key, the data is just noise.</div>
<div class="layer-details">
<h4>Cipher Selection (auto-detected at install)</h4>
<ul>
<li><b>With AES-NI:</b> <code>aes-xts-plain64</code> (AES-256-XTS) &mdash; hardware-accelerated, fastest option</li>
<li><b>Without AES-NI:</b> <code>xchacha20,aes-adiantum-plain64</code> (ChaCha20-Adiantum) &mdash; fast on any CPU</li>
</ul>
<h4>Key Derivation</h4>
<ul>
<li><b>PBKDF:</b> Argon2id (memory-hard, GPU-resistant)</li>
<li><b>Key size:</b> 512 bits</li>
<li><b>Key file:</b> <code>/root/.luks-archipelago.key</code> (4KB random, auto-generated)</li>
</ul>
<h4>What's Encrypted</h4>
<ul>
<li>Bitcoin blockchain data</li>
<li>LND wallet &amp; Lightning channels</li>
<li>All database volumes (PostgreSQL, MariaDB)</li>
<li>All app data directories</li>
<li>Secrets (RPC passwords, macaroons, API keys)</li>
<li>Tor hidden service keys</li>
</ul>
<h4>What's NOT Encrypted</h4>
<ul>
<li>Root filesystem (OS binaries, system config) &mdash; no sensitive data here</li>
<li>EFI/boot partitions (must be readable to start)</li>
</ul>
</div>
</div>
<div class="layer l-net" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">4. Network Layer</span>
<span class="layer-tag badge-cyan">Network</span>
</div>
<div class="layer-desc">Nginx reverse proxy, Tailscale mesh VPN, Tor hidden services, UFW firewall</div>
<div class="layer-layman">One front door (nginx) for all traffic. Tor lets people reach you without knowing your real address. Tailscale lets YOU reach the box from anywhere.</div>
<div class="layer-details">
<h4>Nginx Reverse Proxy</h4>
<ul>
<li>Listens on <code>:80</code> (HTTP) and <code>:443</code> (HTTPS with self-signed cert)</li>
<li>Serves Vue.js SPA at <code>/</code></li>
<li>Proxies backend at <code>/rpc/v1</code>, <code>/ws</code>, <code>/health</code></li>
<li>Proxies each app at <code>/app/{name}/</code></li>
<li>Rate limits: auth (3/s), RPC (20/s), P2P (10/s)</li>
<li>Security headers: CSP, HSTS, X-Frame-Options, Permissions-Policy</li>
<li>Injects <code>nostr-provider.js</code> into all app iframes</li>
</ul>
<h4>Tor</h4>
<ul>
<li>System-level Tor daemon (not containerized)</li>
<li>SOCKS5 proxy at <code>127.0.0.1:9050</code></li>
<li>Hidden services for: web UI, LND, BTCPay, Mempool, Fedimint</li>
<li>Backend manages services via privileged helper script</li>
<li>Containers connect via <code>host.containers.internal:9050</code></li>
</ul>
<h4>Tailscale</h4>
<ul>
<li>Mesh VPN &mdash; access your node from anywhere via encrypted tunnel</li>
<li>Runs as system service or container</li>
<li>Provides stable IP (e.g., <code>100.x.x.x</code>) regardless of network</li>
</ul>
<h4>Firewall (UFW)</h4>
<ul>
<li><code>DEFAULT_FORWARD_POLICY=ACCEPT</code> (required for rootless Podman)</li>
<li>Allow: 22 (SSH), 80 (HTTP), 443 (HTTPS), 8333 (Bitcoin P2P), 9735 (Lightning P2P)</li>
</ul>
</div>
</div>
<div class="layer l-svc" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">5. Rust Backend</span>
<span class="layer-tag badge-blue">Service</span>
</div>
<div class="layer-desc">Archipelago binary &mdash; JSON-RPC server, auth, RBAC, container management, Tor control, DID identity</div>
<div class="layer-layman">The control center. Every button you click in the dashboard sends a message here, and it makes things happen.</div>
<div class="layer-details">
<h4>Bind</h4>
<ul>
<li><code>127.0.0.1:5678</code> &mdash; localhost only, nginx handles external access</li>
</ul>
<h4>Endpoints</h4>
<ul>
<li><code>POST /rpc/v1</code> &mdash; JSON-RPC 2.0 (all commands)</li>
<li><code>WS /ws</code> &mdash; WebSocket (live updates, container status, logs)</li>
<li><code>GET /health</code> &mdash; Health check (no auth)</li>
<li><code>/archipelago/</code> &mdash; P2P node messaging</li>
<li><code>/content</code> &mdash; Content sharing (via Tor)</li>
<li><code>/dwn</code> &mdash; Decentralized Web Node protocol</li>
</ul>
<h4>Key RPC Methods</h4>
<ul>
<li><code>auth.*</code> &mdash; login, TOTP, password change, onboarding</li>
<li><code>seed.*</code> &mdash; generate, verify, restore wallet seeds</li>
<li><code>package.*</code> &mdash; container CRUD (create, start, stop, remove)</li>
<li><code>node.*</code> &mdash; DID identity, signing, backups</li>
<li><code>app.*</code> &mdash; marketplace, app config</li>
</ul>
<h4>Systemd Service</h4>
<ul>
<li>Type: <code>notify</code> (signals readiness to systemd)</li>
<li>Watchdog: 300s (must ping every 120s or gets killed)</li>
<li>MemoryMax: 4GB</li>
<li>Crash recovery on startup (detects unclean shutdown, restarts containers)</li>
<li>Periodic container state snapshots for recovery</li>
</ul>
</div>
</div>
<div class="layer l-cnt" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">6. Container Layer &mdash; Rootless Podman</span>
<span class="layer-tag badge-green">Isolation</span>
</div>
<div class="layer-desc">34 containers, custom bridge network (archy-net), UID mapping, security caps, memory limits</div>
<div class="layer-layman">Apps run in sealed boxes. They can only see what we let them see, use only the memory we allow, and can't mess with each other.</div>
<div class="layer-details">
<h4>Rootless Podman</h4>
<ul>
<li>All containers run as user <code>archipelago</code> (UID 1000)</li>
<li>No root access required &mdash; even if a container is compromised, it can't escalate to root</li>
<li>Subuid/subgid: <code>archipelago:100000:65536</code></li>
<li>Socket: <code>/run/user/1000/podman/podman.sock</code></li>
</ul>
<h4>Networks</h4>
<ul>
<li><b>archy-net</b> (custom bridge) &mdash; Bitcoin stack + services, containers can reach each other by name (DNS)</li>
<li><b>bridge</b> (default) &mdash; standalone apps, port-mapped only</li>
<li><b>host</b> &mdash; Tailscale only (needs full network access)</li>
</ul>
<h4>Security Defaults (per container)</h4>
<ul>
<li><code>--cap-drop=ALL</code> then add only what's needed (least privilege)</li>
<li><code>--security-opt=no-new-privileges</code></li>
<li>Memory limits (128MB to 4GB depending on app)</li>
<li>Health checks with auto-restart on failure</li>
<li>Read-only root filesystem where possible (<code>--read-only</code>)</li>
</ul>
<h4>UID Mapping (inside container &rarr; host)</h4>
<ul>
<li>root (0) &rarr; host UID 100000</li>
<li>postgres (70) &rarr; host UID 100070</li>
<li>bitcoin (101) &rarr; host UID 100101</li>
<li>grafana (472) &rarr; host UID 100472</li>
<li>mariadb (999) &rarr; host UID 100999</li>
</ul>
<h4>Registry</h4>
<ul>
<li>Private registry at <code>git.tx1138.com/lfg2025/</code></li>
<li>HTTPS (self-hosted Gitea)</li>
<li>All images pre-pulled into registry; nodes pull on first boot</li>
</ul>
</div>
</div>
<div class="layer l-ui" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">7. Web UI &mdash; Vue.js SPA</span>
<span class="layer-tag badge-purple">Application</span>
</div>
<div class="layer-desc">Vue 3 + TypeScript + Pinia + Vite, served as static files by nginx</div>
<div class="layer-layman">The website that runs on the box. Open it in any browser on your network to manage everything.</div>
<div class="layer-details">
<h4>Tech Stack</h4>
<ul>
<li><b>Framework:</b> Vue 3 with <code>&lt;script setup lang="ts"&gt;</code></li>
<li><b>State:</b> Pinia stores</li>
<li><b>Bundler:</b> Vite 7</li>
<li><b>Styling:</b> Global CSS with Tailwind utility classes in style.css</li>
</ul>
<h4>Communication</h4>
<ul>
<li><b>JSON-RPC:</b> All commands go through <code>rpc-client.ts</code> &rarr; <code>POST /rpc/v1</code></li>
<li><b>WebSocket:</b> Real-time container status, logs, events via <code>/ws</code></li>
<li><b>CSRF:</b> Token in cookie + <code>X-CSRF-Token</code> header</li>
<li><b>Session:</b> HttpOnly cookie, <code>SameSite=Lax</code></li>
<li><b>Retry:</b> Auto-retry 3x with exponential backoff on 502/503</li>
<li><b>Timeout:</b> 15s default (configurable per call)</li>
</ul>
<h4>Key Views</h4>
<ul>
<li><code>/</code> &mdash; Dashboard (system status, apps)</li>
<li><code>/kiosk</code> &mdash; Kiosk mode (public, no auth)</li>
<li><code>/kiosk-recovery</code> &mdash; Fallback with IP + QR code</li>
<li><code>/marketplace</code> &mdash; App installer</li>
<li><code>/settings</code> &mdash; System configuration</li>
</ul>
<h4>App Embedding</h4>
<ul>
<li>Apps open as iframes via <code>/app/{name}/</code> proxy paths</li>
<li>Each iframe gets <code>nostr-provider.js</code> injected for identity</li>
</ul>
</div>
</div>
<div class="layer l-kiosk" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">8. Kiosk Display</span>
<span class="layer-tag badge-amber">Physical</span>
</div>
<div class="layer-desc">X11 + Chromium in kiosk mode on VT7, auto-start, crash recovery</div>
<div class="layer-layman">Plug in a monitor and the dashboard appears fullscreen. No login, no desktop, just your node. Press Ctrl+Alt+F1 for a terminal.</div>
<div class="layer-details">
<h4>How It Works</h4>
<ul>
<li>X11 server (Xorg) starts on Virtual Terminal 7</li>
<li>Chromium launches in <code>--kiosk --app=http://localhost/kiosk</code> mode</li>
<li>No address bar, no tabs, no right-click &mdash; just the dashboard</li>
<li>Cursor hidden after 3 seconds of inactivity</li>
<li>Screen blanking disabled</li>
</ul>
<h4>Resource Limits</h4>
<ul>
<li><code>--disable-gpu</code> &mdash; software rendering only</li>
<li><code>--renderer-process-limit=1</code> &mdash; single renderer process</li>
<li><code>--js-flags="--max-old-space-size=128"</code> &mdash; 128MB JS heap max</li>
<li><code>--disable-metrics-reporting</code> &mdash; no telemetry to Google</li>
<li><code>--enable-low-end-device-mode</code> &mdash; reduce animations and compositing</li>
</ul>
<h4>Controls</h4>
<ul>
<li><code>Ctrl+Alt+F7</code> &mdash; switch to kiosk</li>
<li><code>Ctrl+Alt+F1</code> &mdash; switch to terminal</li>
<li><code>sudo archipelago-kiosk enable|disable|toggle|status</code></li>
</ul>
</div>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- CONTAINERS -->
<!-- ============================================================ -->
<div class="section" id="sec-containers">
<div class="controls">
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="tier-0">Databases</button>
<button class="filter-btn" data-filter="tier-1">Core Bitcoin</button>
<button class="filter-btn" data-filter="tier-2">Services</button>
<button class="filter-btn" data-filter="tier-3">Apps</button>
<button class="filter-btn" data-filter="tier-4">IndeedHub</button>
<button class="filter-btn" data-filter="tier-5">Penpot</button>
<button class="filter-btn" data-filter="archy-net">archy-net</button>
<button class="filter-btn" data-filter="bridge">bridge</button>
</div>
<!-- Tier 0: Databases -->
<div class="tier-section tier-0" data-tier="tier-0">
<div class="tier-label">Tier 0 &mdash; Databases</div>
<div class="cards">
<div class="card" data-name="archy-mempool-db" data-tier="tier-0" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">archy-mempool-db</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">MariaDB database storing Bitcoin mempool transaction data for the Mempool block explorer.</div>
<div class="card-layman">A database that remembers pending Bitcoin transactions so the block explorer can show them.</div>
<div class="card-image">mariadb:11.4.10</div>
<div class="card-ports">No exposed ports (internal only)</div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">UID</span><span class="detail-value">100999:100999 (mariadb user)</span></div>
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">512 MB</span></div>
<div class="detail-row"><span class="detail-label">Health</span><span class="detail-value"><code>mariadb -uroot -e 'SELECT 1'</code></span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/mysql-mempool</span></div>
<div class="detail-row"><span class="detail-label">Database</span><span class="detail-value">mempool (user: mempool)</span></div>
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value">None</span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('mempool-api')">mempool-api</span></span></div>
</div>
</div>
<div class="card" data-name="archy-btcpay-db" data-tier="tier-0" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">archy-btcpay-db</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">PostgreSQL database for BTCPay Server and NBXplorer, storing invoices, transactions, and merchant data.</div>
<div class="card-layman">Stores your payment invoices and transaction history for the Bitcoin payment processor.</div>
<div class="card-image">postgres:15.17</div>
<div class="card-ports">No exposed ports (internal only)</div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">UID</span><span class="detail-value">100070:100070 (postgres user)</span></div>
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">512 MB</span></div>
<div class="detail-row"><span class="detail-label">Health</span><span class="detail-value"><code>pg_isready -U postgres</code></span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/postgres-btcpay</span></div>
<div class="detail-row"><span class="detail-label">Databases</span><span class="detail-value">btcpay, nbxplorer</span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('archy-nbxplorer')">nbxplorer</span> <span class="dep-tag" onclick="find('btcpay-server')">btcpay</span></span></div>
</div>
</div>
<div class="card" data-name="indeedhub-postgres" data-tier="tier-0 tier-4" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">indeedhub-postgres</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">PostgreSQL database for IndeedHub social platform, storing posts, user profiles, and relay data.</div>
<div class="card-layman">The database that stores all the social media posts and user data for IndeedHub.</div>
<div class="card-image">postgres:16.13-alpine</div>
<div class="card-ports">No exposed ports (internal only)</div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('indeedhub-api')">indeedhub-api</span></span></div>
</div>
</div>
<div class="card" data-name="indeedhub-redis" data-tier="tier-0 tier-4" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">indeedhub-redis</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">Redis in-memory cache for IndeedHub, handling sessions, job queues, and real-time data.</div>
<div class="card-layman">A fast temporary memory store so IndeedHub pages load quickly and background tasks run smoothly.</div>
<div class="card-image">redis:7.4.8-alpine</div>
<div class="card-ports">No exposed ports</div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('indeedhub-api')">indeedhub-api</span></span></div>
</div>
</div>
<div class="card" data-name="penpot-postgres" data-tier="tier-0 tier-5" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">penpot-postgres</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">PostgreSQL database for Penpot design tool, storing projects, layers, and design assets.</div>
<div class="card-layman">Stores all the design projects and files for the Penpot design tool.</div>
<div class="card-image">postgres:15</div>
<div class="card-ports">No exposed ports</div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">256 MB</span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('penpot-backend')">penpot-backend</span></span></div>
</div>
</div>
<div class="card" data-name="penpot-valkey" data-tier="tier-0 tier-5" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">penpot-valkey</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">Valkey (Redis fork) cache for Penpot, handling sessions and real-time collaboration sync.</div>
<div class="card-layman">Fast memory cache that makes Penpot's real-time collaboration work smoothly.</div>
<div class="card-image">valkey:8.1</div>
<div class="card-ports">No exposed ports</div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">128 MB</span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('penpot-backend')">penpot-backend</span></span></div>
</div>
</div>
<div class="card" data-name="immich_postgres" data-tier="tier-0" data-net="bridge" onclick="toggle(this)" style="opacity:0.5">
<div class="card-header"><span class="card-name">immich_postgres</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">PostgreSQL with vector extensions for Immich photo AI/search. Optional &mdash; only if Immich is installed.</div>
<div class="card-layman">Database for the photo manager. Has special AI search features for finding photos by what's in them.</div>
<div class="card-image">immich-postgres:14-vectorchord (optional)</div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">256 MB</span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('immich_server')">immich</span></span></div>
</div>
</div>
<div class="card" data-name="immich_redis" data-tier="tier-0" data-net="bridge" onclick="toggle(this)" style="opacity:0.5">
<div class="card-header"><span class="card-name">immich_redis</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">Valkey cache for Immich job queue (photo processing, thumbnail generation).</div>
<div class="card-layman">Manages the queue of photos waiting to be processed and thumbnailed.</div>
<div class="card-image">valkey:8.1.6 (optional)</div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">128 MB</span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('immich_server')">immich</span></span></div>
</div>
</div>
</div>
</div>
<!-- Tier 1: Core Bitcoin -->
<div class="tier-section tier-1" data-tier="tier-1">
<div class="tier-label">Tier 1 &mdash; Core Bitcoin Infrastructure</div>
<div class="cards">
<div class="card" data-name="bitcoin-knots" data-tier="tier-1" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">bitcoin-knots</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">Full Bitcoin node (Knots variant). Validates every transaction and block independently. The root dependency for the entire Bitcoin stack.</div>
<div class="card-layman">Your own copy of the entire Bitcoin network. Nobody can lie to you about your balance because you verify everything yourself.</div>
<div class="card-image">bitcoin-knots:latest</div>
<div class="card-ports">Ports: <span>8332</span> <span>8333</span> <span>28332</span> <span>28333</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Port 8332</span><span class="detail-value">JSON-RPC API (how other apps talk to Bitcoin)</span></div>
<div class="detail-row"><span class="detail-label">Port 8333</span><span class="detail-value">P2P network (connects to other Bitcoin nodes worldwide)</span></div>
<div class="detail-row"><span class="detail-label">Port 28332</span><span class="detail-value">ZMQ block notifications (instant alert when new block arrives)</span></div>
<div class="detail-row"><span class="detail-label">Port 28333</span><span class="detail-value">ZMQ transaction notifications (instant alert for new transactions)</span></div>
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">2 GB (1 GB on low-memory systems)</span></div>
<div class="detail-row"><span class="detail-label">Health</span><span class="detail-value"><code>bitcoin-cli getblockchaininfo</code></span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/bitcoin (~500GB full, ~550MB pruned)</span></div>
<div class="detail-row"><span class="detail-label">Disk mode</span><span class="detail-value">Auto: prune if &lt;1TB, full txindex if &ge;1TB</span></div>
<div class="detail-row"><span class="detail-label">RPC Auth</span><span class="detail-value">HMAC-SHA256 salted hash (no plaintext password in config)</span></div>
<div class="detail-row"><span class="detail-label">Tor</span><span class="detail-value">Routes P2P through Tor SOCKS5 for privacy</span></div>
<div class="detail-row"><span class="detail-label">Caps</span><span class="detail-value">CHOWN, FOWNER, SETUID, SETGID, DAC_OVERRIDE</span></div>
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value">None &mdash; ROOT DEPENDENCY</span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('electrumx')">electrumx</span> <span class="dep-tag" onclick="find('lnd')">lnd</span> <span class="dep-tag" onclick="find('mempool-api')">mempool</span> <span class="dep-tag" onclick="find('archy-nbxplorer')">nbxplorer</span> <span class="dep-tag" onclick="find('fedimint')">fedimint</span></span></div>
</div>
</div>
<div class="card" data-name="electrumx" data-tier="tier-1" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">electrumx</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">Electrum protocol server. Indexes the blockchain by address so wallets can look up balances instantly without scanning every block.</div>
<div class="card-layman">An index for Bitcoin. Like a book's table of contents &mdash; instead of reading every page to find your info, you jump straight to it.</div>
<div class="card-image">electrumx:v1.18.0</div>
<div class="card-ports">Ports: <span>50001</span> <span>8000</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Port 50001</span><span class="detail-value">Electrum protocol (wallet connections)</span></div>
<div class="detail-row"><span class="detail-label">Port 8000</span><span class="detail-value">Health check / status API</span></div>
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">1 GB</span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/electrumx</span></div>
<div class="detail-row"><span class="detail-label">Protocol</span><span class="detail-value">Electrum JSON-RPC over TCP</span></div>
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('bitcoin-knots')">bitcoin-knots</span> (reads blockchain via RPC)</span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('mempool-api')">mempool-api</span></span></div>
</div>
</div>
</div>
</div>
<!-- Tier 2: Services -->
<div class="tier-section tier-2" data-tier="tier-2">
<div class="tier-label">Tier 2 &mdash; Services (depend on Bitcoin core)</div>
<div class="cards">
<div class="card" data-name="lnd" data-tier="tier-2" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">lnd</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">Lightning Network Daemon. Enables instant, low-fee Bitcoin payments through payment channels.</div>
<div class="card-layman">Lets you send and receive Bitcoin instantly (instead of waiting 10+ minutes for a block). Like a tab at a bar &mdash; settle up later on-chain.</div>
<div class="card-image">lnd:v0.18.4-beta</div>
<div class="card-ports">Ports: <span>9735</span> <span>10009</span> <span>8080</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Port 9735</span><span class="detail-value">Lightning P2P (connects to other Lightning nodes)</span></div>
<div class="detail-row"><span class="detail-label">Port 10009</span><span class="detail-value">gRPC API (admin operations, authenticated with macaroons)</span></div>
<div class="detail-row"><span class="detail-label">Port 8080</span><span class="detail-value">REST API (simpler HTTP interface to LND)</span></div>
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">512 MB</span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/lnd (wallet, channels, macaroons)</span></div>
<div class="detail-row"><span class="detail-label">Auth</span><span class="detail-value">Macaroon tokens (read-only for queries, admin for mutations)</span></div>
<div class="detail-row"><span class="detail-label">Tor</span><span class="detail-value">Active with stream isolation (each connection uses different circuit)</span></div>
<div class="detail-row"><span class="detail-label">Caps</span><span class="detail-value">CHOWN, FOWNER, SETUID, SETGID, DAC_OVERRIDE, NET_RAW</span></div>
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('bitcoin-knots')">bitcoin-knots</span></span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('fedimint-gateway')">fedi-gateway (LND mode)</span> <span class="dep-tag" onclick="find('archy-lnd-ui')">lnd-ui</span></span></div>
</div>
</div>
<div class="card" data-name="mempool-api" data-tier="tier-2" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">mempool-api</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">Mempool.space backend API. Provides blockchain analytics, fee estimates, and transaction tracking.</div>
<div class="card-layman">The engine behind the block explorer. Shows you what's happening on the Bitcoin network in real time.</div>
<div class="card-image">mempool-backend:v3.0.0</div>
<div class="card-ports">Ports: <span>8999</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">512 MB</span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/mempool</span></div>
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('bitcoin-knots')">bitcoin-knots</span> <span class="dep-tag" onclick="find('electrumx')">electrumx</span> <span class="dep-tag" onclick="find('archy-mempool-db')">mempool-db</span></span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('archy-mempool-web')">mempool-web</span></span></div>
</div>
</div>
<div class="card" data-name="archy-mempool-web" data-tier="tier-2" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">archy-mempool-web</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">Mempool.space frontend. The visual block explorer with real-time mempool visualization and fee graphs.</div>
<div class="card-layman">Your personal mempool.space &mdash; watch Bitcoin blocks being mined, see fee rates, track your transactions.</div>
<div class="card-image">mempool-frontend:v3.0.0</div>
<div class="card-ports">Ports: <span>4080</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">256 MB</span></div>
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('mempool-api')">mempool-api</span></span></div>
<div class="detail-row"><span class="detail-label">Nginx path</span><span class="detail-value">/app/mempool/</span></div>
</div>
</div>
<div class="card" data-name="archy-nbxplorer" data-tier="tier-2" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">archy-nbxplorer</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">NBXplorer blockchain scanner. Watches Bitcoin addresses for BTCPay and notifies when payments arrive.</div>
<div class="card-layman">Watches Bitcoin for incoming payments and tells BTCPay Server when money arrives for your invoices.</div>
<div class="card-image">nbxplorer:2.6.0</div>
<div class="card-ports">Ports: <span>32838</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">512 MB</span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/nbxplorer</span></div>
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('bitcoin-knots')">bitcoin-knots</span> <span class="dep-tag" onclick="find('archy-btcpay-db')">btcpay-db</span></span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('btcpay-server')">btcpay</span></span></div>
</div>
</div>
<div class="card" data-name="btcpay-server" data-tier="tier-2" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">btcpay-server</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">Self-hosted Bitcoin payment processor. Accept Bitcoin payments with invoices, checkout pages, and POS.</div>
<div class="card-layman">Your own payment terminal for Bitcoin. Create invoices, get paid, no middleman taking a cut.</div>
<div class="card-image">btcpayserver:1.13.7</div>
<div class="card-ports">Ports: <span>23000</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">1 GB</span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/btcpay</span></div>
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('archy-nbxplorer')">nbxplorer</span> <span class="dep-tag" onclick="find('archy-btcpay-db')">btcpay-db</span></span></div>
<div class="detail-row"><span class="detail-label">Nginx path</span><span class="detail-value">/app/btcpay/</span></div>
<div class="detail-row"><span class="detail-label">Tor</span><span class="detail-value">Has its own .onion address for receiving payments privately</span></div>
</div>
</div>
<div class="card" data-name="fedimint" data-tier="tier-2" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">fedimint</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">Federated mint daemon. Enables community-run Bitcoin custody with threshold signing and e-cash tokens.</div>
<div class="card-layman">A way for a group of trusted people to collectively hold Bitcoin. No single person can steal the funds &mdash; you need a majority to approve.</div>
<div class="card-image">fedimintd:v0.10.0</div>
<div class="card-ports">Ports: <span>8173</span> <span>8174</span> <span>8175</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Port 8173</span><span class="detail-value">P2P (federation member communication)</span></div>
<div class="detail-row"><span class="detail-label">Port 8174</span><span class="detail-value">API / WebSocket (client connections)</span></div>
<div class="detail-row"><span class="detail-label">Port 8175</span><span class="detail-value">Web UI (guardian dashboard)</span></div>
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">512 MB</span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/fedimint</span></div>
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('bitcoin-knots')">bitcoin-knots</span></span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('fedimint-gateway')">fedi-gateway</span></span></div>
</div>
</div>
<div class="card" data-name="fedimint-gateway" data-tier="tier-2" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">fedimint-gateway</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">Lightning bridge for Fedimint. Connects the federation to the Lightning Network for instant payments.</div>
<div class="card-layman">Connects your community mint to Lightning so federation members can send/receive instant payments.</div>
<div class="card-image">gatewayd:v0.10.0</div>
<div class="card-ports">Ports: <span>8176</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">512 MB</span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/fedimint-gateway</span></div>
<div class="detail-row"><span class="detail-label">Mode</span><span class="detail-value">Auto-detect: uses LND if available, otherwise built-in LDK Lightning</span></div>
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('bitcoin-knots')">bitcoin-knots</span> <span class="dep-tag" onclick="find('fedimint')">fedimint</span></span></div>
</div>
</div>
<div class="card" data-name="immich_server" data-tier="tier-2" data-net="bridge" onclick="toggle(this)" style="opacity:0.5">
<div class="card-header"><span class="card-name">immich_server</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">Self-hosted Google Photos replacement with AI-powered search, face detection, and automatic organization.</div>
<div class="card-layman">Like Google Photos but on your own hardware. Your photos never leave your box. AI finds faces and objects locally.</div>
<div class="card-image">immich-server:release (optional)</div>
<div class="card-ports">Ports: <span>2283</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('immich_postgres')">immich_postgres</span> <span class="dep-tag" onclick="find('immich_redis')">immich_redis</span></span></div>
<div class="detail-row"><span class="detail-label">Nginx path</span><span class="detail-value">/app/immich/</span></div>
</div>
</div>
</div>
</div>
<!-- Tier 3: Apps -->
<div class="tier-section tier-3" data-tier="tier-3">
<div class="tier-label">Tier 3 &mdash; Applications (independent, no cross-dependencies)</div>
<div class="cards">
<div class="card" data-name="archy-bitcoin-ui" data-tier="tier-3" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">archy-bitcoin-ui</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">Custom Bitcoin node dashboard showing sync status, peer connections, and blockchain info.</div>
<div class="card-layman">A pretty dashboard for your Bitcoin node. See how synced you are, how many peers you have, block height.</div>
<div class="card-image">bitcoin-ui:latest</div>
<div class="card-ports">Ports: <span>8334</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">128 MB</span></div>
<div class="detail-row"><span class="detail-label">Nginx path</span><span class="detail-value">/app/bitcoin-ui/</span></div>
</div>
</div>
<div class="card" data-name="archy-lnd-ui" data-tier="tier-3" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">archy-lnd-ui</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">Custom Lightning dashboard showing channels, balances, routing stats, and payment history.</div>
<div class="card-layman">Dashboard for your Lightning node. See your channels, balance, and recent payments at a glance.</div>
<div class="card-image">lnd-ui:latest</div>
<div class="card-ports">Ports: <span>8081</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">128 MB</span></div>
<div class="detail-row"><span class="detail-label">Nginx path</span><span class="detail-value">/app/lnd/</span></div>
</div>
</div>
<div class="card" data-name="archy-electrs-ui" data-tier="tier-3" data-net="host" onclick="toggle(this)">
<div class="card-header"><span class="card-name">archy-electrs-ui</span><span class="card-badge badge-amber">host</span></div>
<div class="card-desc">ElectrumX status dashboard showing sync progress, connected clients, and index health.</div>
<div class="card-layman">Shows whether the Electrum index is synced and healthy. How far behind it is, how many wallets are connected.</div>
<div class="card-image">electrs-ui:latest</div>
<div class="card-ports">Ports: <span>50002</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">128 MB</span></div>
<div class="detail-row"><span class="detail-label">Network</span><span class="detail-value">host (needs direct access to localhost:50001)</span></div>
<div class="detail-row"><span class="detail-label">Nginx path</span><span class="detail-value">/app/electrumx/</span></div>
</div>
</div>
<div class="card" data-name="homeassistant" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">homeassistant</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">Open-source home automation platform. Control lights, sensors, cameras, and IoT devices from one dashboard.</div>
<div class="card-layman">Smart home control center. Turn lights on, check sensors, automate your house &mdash; all locally, no cloud needed.</div>
<div class="card-image">home-assistant:2024.1</div>
<div class="card-ports">Ports: <span>8123</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">512 MB</span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/home-assistant</span></div>
<div class="detail-row"><span class="detail-label">Nginx path</span><span class="detail-value">/app/homeassistant/</span></div>
</div>
</div>
<div class="card" data-name="grafana" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">grafana</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">Monitoring and visualization platform. Dashboards for system metrics, Bitcoin stats, and container health.</div>
<div class="card-layman">Beautiful graphs and charts showing how your system is doing. CPU, memory, Bitcoin sync, everything visualized.</div>
<div class="card-image">grafana:10.2.0</div>
<div class="card-ports">Ports: <span>3000</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">UID</span><span class="detail-value">100472:100472 (grafana user)</span></div>
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">256 MB</span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/grafana</span></div>
<div class="detail-row"><span class="detail-label">Read-only</span><span class="detail-value">Yes (tmpfs for /tmp, /run)</span></div>
<div class="detail-row"><span class="detail-label">Nginx path</span><span class="detail-value">/app/grafana/</span></div>
</div>
</div>
<div class="card" data-name="uptime-kuma" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">uptime-kuma</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">Self-hosted uptime monitor. Pings your services and alerts you when something goes down.</div>
<div class="card-layman">Watches all your apps and sends alerts if anything stops working. Like a security guard for your services.</div>
<div class="card-image">uptime-kuma:1</div>
<div class="card-ports">Ports: <span>3001</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">256 MB</span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/uptime-kuma</span></div>
</div>
</div>
<div class="card" data-name="jellyfin" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">jellyfin</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">Self-hosted media server. Stream your movies, TV shows, and music from your own hardware.</div>
<div class="card-layman">Your own Netflix. Put movies on the box, watch them on any device. No subscription, no limits.</div>
<div class="card-image">jellyfin:10.8.13</div>
<div class="card-ports">Ports: <span>8096</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">1 GB</span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/jellyfin/{config,cache}</span></div>
<div class="detail-row"><span class="detail-label">Transcode</span><span class="detail-value">tmpfs /tmp (256MB, rw,exec)</span></div>
<div class="detail-row"><span class="detail-label">Nginx path</span><span class="detail-value">/app/jellyfin/</span></div>
</div>
</div>
<div class="card" data-name="photoprism" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">photoprism</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">AI-powered photo management. Automatic face recognition, location mapping, and smart search.</div>
<div class="card-layman">Photo organizer that uses AI to tag and sort your pictures. Find photos by searching "sunset" or "cat."</div>
<div class="card-image">photoprism:240915</div>
<div class="card-ports">Ports: <span>2342</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">1 GB (512 MB on low-memory)</span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/photoprism</span></div>
</div>
</div>
<div class="card" data-name="vaultwarden" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">vaultwarden</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">Bitwarden-compatible password manager. Store all your passwords encrypted, sync across devices.</div>
<div class="card-layman">Your personal password safe. Store every password securely and auto-fill them on your phone and computer.</div>
<div class="card-image">vaultwarden:1.30.0-alpine</div>
<div class="card-ports">Ports: <span>8082</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">256 MB</span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/vaultwarden</span></div>
<div class="detail-row"><span class="detail-label">Caps</span><span class="detail-value">CHOWN, SETUID, SETGID, NET_BIND_SERVICE</span></div>
</div>
</div>
<div class="card" data-name="nextcloud" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">nextcloud</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">Self-hosted file sync and collaboration platform. Dropbox/Google Drive replacement with calendar, contacts, and office docs.</div>
<div class="card-layman">Your own Dropbox. Sync files, share documents, manage calendar and contacts &mdash; all on your own hardware.</div>
<div class="card-image">nextcloud:29</div>
<div class="card-ports">Ports: <span>8085</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">1 GB</span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/nextcloud</span></div>
</div>
</div>
<div class="card" data-name="searxng" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">searxng</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">Privacy-respecting metasearch engine. Searches Google, Bing, DuckDuckGo and others without tracking you.</div>
<div class="card-layman">Private search engine. Searches the web without anyone tracking what you look for.</div>
<div class="card-image">searxng:latest</div>
<div class="card-ports">Ports: <span>8888</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">512 MB</span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/searxng</span></div>
<div class="detail-row"><span class="detail-label">Read-only</span><span class="detail-value">Yes (tmpfs for /tmp, /run)</span></div>
</div>
</div>
<div class="card" data-name="onlyoffice" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">onlyoffice</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">Self-hosted document editor. Edit Word, Excel, and PowerPoint files collaboratively in the browser.</div>
<div class="card-layman">Like Google Docs but on your own box. Edit spreadsheets and documents with others in real time.</div>
<div class="card-image">onlyoffice:latest</div>
<div class="card-ports">Ports: <span>9980</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">2 GB (1 GB on low-memory)</span></div>
</div>
</div>
<div class="card" data-name="ollama" data-tier="tier-3" data-net="bridge" onclick="toggle(this)" style="opacity:0.5">
<div class="card-header"><span class="card-name">ollama</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">Local AI model runner. Run LLMs (like Llama, Mistral) entirely on your hardware, no cloud needed.</div>
<div class="card-layman">ChatGPT on your own box. Talk to AI privately &mdash; nothing you say leaves your machine.</div>
<div class="card-image">ollama:latest (optional)</div>
<div class="card-ports">Ports: <span>11434</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">4 GB (1 GB on low-memory)</span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/ollama</span></div>
<div class="detail-row"><span class="detail-label">Read-only</span><span class="detail-value">Yes (tmpfs for /tmp, /run)</span></div>
<div class="detail-row"><span class="detail-label">Protocol</span><span class="detail-value">REST API at :11434 (OpenAI-compatible)</span></div>
</div>
</div>
<div class="card" data-name="filebrowser" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">filebrowser</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">Web-based file manager. Browse, upload, and download files through the browser.</div>
<div class="card-layman">A file explorer in your browser. Upload, download, and manage files on the box without SSH.</div>
<div class="card-image">filebrowser:v2.27.0</div>
<div class="card-ports">Ports: <span>8083</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">256 MB</span></div>
<div class="detail-row"><span class="detail-label">Data</span><span class="detail-value">/var/lib/archipelago/filebrowser (served), filebrowser-data (DB)</span></div>
<div class="detail-row"><span class="detail-label">Read-only</span><span class="detail-value">Yes</span></div>
<div class="detail-row"><span class="detail-label">Max upload</span><span class="detail-value">10 GB (nginx limit)</span></div>
</div>
</div>
<div class="card" data-name="nginx-proxy-manager" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">nginx-proxy-manager</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">GUI for managing nginx proxy rules and SSL certificates. Point-and-click reverse proxy configuration.</div>
<div class="card-layman">A visual tool for routing web traffic. Point domains to services and manage HTTPS certificates with clicks, not config files.</div>
<div class="card-image">nginx-proxy-manager:latest</div>
<div class="card-ports">Ports: <span>81</span> <span>8084</span> <span>8443</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Port 81</span><span class="detail-value">Admin dashboard</span></div>
<div class="detail-row"><span class="detail-label">Port 8084</span><span class="detail-value">HTTP proxy</span></div>
<div class="detail-row"><span class="detail-label">Port 8443</span><span class="detail-value">HTTPS proxy</span></div>
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">256 MB</span></div>
</div>
</div>
<div class="card" data-name="portainer" data-tier="tier-3" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">portainer</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">Container management UI. Visual dashboard for Podman containers &mdash; start, stop, inspect, view logs.</div>
<div class="card-layman">Visual control panel for all your containers. See what's running, restart things, read logs &mdash; no terminal needed.</div>
<div class="card-image">portainer:latest</div>
<div class="card-ports">Ports: <span>9000</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">256 MB</span></div>
<div class="detail-row"><span class="detail-label">Socket</span><span class="detail-value">Podman socket mounted as Docker socket</span></div>
</div>
</div>
</div>
</div>
<!-- Tier 4: IndeedHub stack -->
<div class="tier-section" data-tier="tier-4">
<div class="tier-label" style="color: #06b6d4;">IndeedHub Stack &mdash; Nostr-based Social Platform</div>
<div class="cards">
<div class="card" data-name="indeedhub-minio" data-tier="tier-4" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">indeedhub-minio</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">S3-compatible object storage for IndeedHub media files (images, videos, attachments).</div>
<div class="card-layman">File storage for IndeedHub. When someone posts an image, it lives here.</div>
<div class="card-image">minio:RELEASE.2024-11-07</div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('indeedhub-api')">indeedhub-api</span></span></div>
</div>
</div>
<div class="card" data-name="indeedhub-api" data-tier="tier-4" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">indeedhub-api</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">IndeedHub backend API. Handles Nostr events, user profiles, media uploads, and relay communication.</div>
<div class="card-layman">The engine behind IndeedHub. Processes posts, handles user accounts, talks to Nostr relays.</div>
<div class="card-image">indeedhub-api (custom build)</div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('indeedhub-postgres')">postgres</span> <span class="dep-tag" onclick="find('indeedhub-redis')">redis</span> <span class="dep-tag" onclick="find('indeedhub-minio')">minio</span></span></div>
<div class="detail-row"><span class="detail-label">Needed by</span><span class="detail-value"><span class="dep-tag" onclick="find('indeedhub')">indeedhub</span></span></div>
</div>
</div>
<div class="card" data-name="indeedhub-ffmpeg" data-tier="tier-4" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">indeedhub-ffmpeg</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">Video transcoding worker for IndeedHub. Converts uploaded videos to web-friendly formats.</div>
<div class="card-layman">Converts videos so they play smoothly in the browser. Like a video format translator.</div>
<div class="card-image">indeedhub-ffmpeg (custom build)</div>
</div>
<div class="card" data-name="indeedhub-relay" data-tier="tier-4" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">indeedhub-relay</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">Nostr relay for IndeedHub. Stores and distributes Nostr events (posts, follows, reactions).</div>
<div class="card-layman">A message board that stores Nostr posts. Other Nostr apps can connect here to read and post.</div>
<div class="card-image">indeedhub-relay (custom build)</div>
</div>
<div class="card" data-name="indeedhub" data-tier="tier-4" data-net="archy-net" onclick="toggle(this)">
<div class="card-header"><span class="card-name">indeedhub</span><span class="card-badge badge-red">archy-net</span></div>
<div class="card-desc">IndeedHub web frontend. Nostr-based social media client with feeds, profiles, messaging, and media.</div>
<div class="card-layman">The social media app itself. Post, follow people, send messages, share media &mdash; all on the Nostr protocol.</div>
<div class="card-image">indeedhub-frontend (custom build)</div>
<div class="card-ports">Ports: <span>7777</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Nginx path</span><span class="detail-value">/app/indeedhub/</span></div>
<div class="detail-row"><span class="detail-label">WebSocket</span><span class="detail-value">Yes (for real-time updates)</span></div>
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('indeedhub-api')">indeedhub-api</span></span></div>
</div>
</div>
</div>
</div>
<!-- Tier 5: Penpot stack -->
<div class="tier-section" data-tier="tier-5">
<div class="tier-label" style="color: #ec4899;">Penpot Stack &mdash; Design Tool</div>
<div class="cards">
<div class="card" data-name="penpot-backend" data-tier="tier-5" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">penpot-backend</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">Penpot application server. Handles design data, real-time collaboration, and file storage.</div>
<div class="card-layman">The engine behind the design tool. Saves your designs and lets multiple people edit at the same time.</div>
<div class="card-image">penpot-backend:2.4</div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">512 MB</span></div>
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('penpot-postgres')">penpot-postgres</span> <span class="dep-tag" onclick="find('penpot-valkey')">penpot-valkey</span></span></div>
</div>
</div>
<div class="card" data-name="penpot-exporter" data-tier="tier-5" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">penpot-exporter</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">Renders Penpot designs to PDF, SVG, and image formats for export.</div>
<div class="card-layman">Turns your designs into downloadable files &mdash; PDFs, images, SVGs.</div>
<div class="card-image">penpot-exporter:2.4</div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">256 MB</span></div>
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('penpot-backend')">penpot-backend</span></span></div>
</div>
</div>
<div class="card" data-name="penpot-frontend" data-tier="tier-5" data-net="bridge" onclick="toggle(this)">
<div class="card-header"><span class="card-name">penpot-frontend</span><span class="card-badge badge-green">bridge</span></div>
<div class="card-desc">Penpot web UI. Open-source Figma alternative with vector editing, prototyping, and collaboration.</div>
<div class="card-layman">Your own Figma. Design interfaces, create prototypes, collaborate &mdash; completely self-hosted.</div>
<div class="card-image">penpot-frontend:2.4</div>
<div class="card-ports">Ports: <span>9001</span></div>
<div class="card-details">
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">256 MB</span></div>
<div class="detail-row"><span class="detail-label">Nginx path</span><span class="detail-value">/app/penpot/</span></div>
<div class="detail-row"><span class="detail-label">Deps</span><span class="detail-value"><span class="dep-tag" onclick="find('penpot-backend')">penpot-backend</span></span></div>
</div>
</div>
</div>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:rgba(239,68,68,0.4)"></div> archy-net (internal DNS, Bitcoin stack)</div>
<div class="legend-item"><div class="legend-dot" style="background:rgba(16,185,129,0.4)"></div> bridge (standalone, port-mapped)</div>
<div class="legend-item"><div class="legend-dot" style="background:rgba(245,158,11,0.4)"></div> host (direct network access)</div>
<div class="legend-item" style="color:#6b7280; opacity:0.5">Dimmed = optional / not always installed</div>
</div>
</div>
<!-- ============================================================ -->
<!-- PROTOCOLS -->
<!-- ============================================================ -->
<div class="section" id="sec-protocols">
<div class="proto-grid">
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">JSON-RPC 2.0</div>
<div class="proto-card-desc">Primary protocol between the web UI and the Rust backend. All commands are RPC calls.</div>
<div class="proto-card-layman">Like texting the backend: you send a message ("please start this app"), it texts back ("done" or "error").</div>
<div class="proto-details">
<b>Endpoint:</b> <code>POST /rpc/v1</code><br>
<b>Format:</b> <code>{"jsonrpc":"2.0","method":"package.start","params":{"id":"bitcoin-knots"},"id":1}</code><br>
<b>Auth:</b> Session cookie + CSRF token header<br>
<b>Timeout:</b> 15s default<br>
<b>Retry:</b> 3 attempts with exponential backoff on 502/503<br>
<b>Rate limit:</b> 20 req/s (burst 40)<br>
<b>Used by:</b> Vue.js frontend &rarr; Rust backend
</div>
</div>
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">WebSocket</div>
<div class="proto-card-desc">Real-time bidirectional channel for live updates &mdash; container status changes, logs, events.</div>
<div class="proto-card-layman">An open phone line between your browser and the server. Instead of asking "any updates?" every second, the server just tells you when something changes.</div>
<div class="proto-details">
<b>Endpoint:</b> <code>WS /ws</code> (HTTP upgrade)<br>
<b>Auth:</b> Session cookie<br>
<b>Read timeout:</b> 86,400s (24 hours)<br>
<b>Events:</b> Container state changes, log streams, system alerts<br>
<b>Used by:</b> Vue.js frontend &harr; Rust backend
</div>
</div>
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">Bitcoin RPC (JSON-RPC 1.0)</div>
<div class="proto-card-desc">How apps talk to the Bitcoin node. Authenticated with username + HMAC-hashed password.</div>
<div class="proto-card-layman">The language apps use to ask Bitcoin questions: "what's the current block?" or "send this transaction."</div>
<div class="proto-details">
<b>Endpoint:</b> <code>bitcoin-knots:8332</code> (inside archy-net)<br>
<b>Auth:</b> HTTP Basic with rpcauth hash (HMAC-SHA256, no plaintext)<br>
<b>Methods:</b> getblockchaininfo, getmempoolinfo, sendrawtransaction, etc.<br>
<b>Timeout:</b> 10s default, 30s for heavy ops<br>
<b>Used by:</b> ElectrumX, LND, Mempool, NBXplorer, Fedimint &rarr; Bitcoin Knots
</div>
</div>
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">gRPC</div>
<div class="proto-card-desc">High-performance RPC protocol used by LND for admin operations. Binary format, strongly typed.</div>
<div class="proto-card-layman">A fast, structured way for apps to control the Lightning node. More efficient than regular HTTP for complex operations.</div>
<div class="proto-details">
<b>Endpoint:</b> <code>lnd:10009</code><br>
<b>Auth:</b> Macaroon tokens (read-only for queries, admin for mutations)<br>
<b>TLS:</b> Self-signed certificate (auto-generated)<br>
<b>Methods:</b> OpenChannel, SendPayment, GetInfo, ListChannels, etc.<br>
<b>Used by:</b> Fedimint Gateway, LND UI &rarr; LND
</div>
</div>
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">Electrum Protocol</div>
<div class="proto-card-desc">Lightweight protocol for wallet address lookups. JSON-RPC over raw TCP sockets.</div>
<div class="proto-card-layman">How Bitcoin wallets check their balance without downloading the entire blockchain. Ask "what transactions touched this address?" and get an instant answer.</div>
<div class="proto-details">
<b>Endpoint:</b> <code>electrumx:50001</code> (TCP)<br>
<b>Format:</b> Newline-delimited JSON-RPC<br>
<b>Methods:</b> blockchain.scripthash.get_balance, blockchain.transaction.get, etc.<br>
<b>Used by:</b> Wallets (Sparrow, Electrum), Mempool API &rarr; ElectrumX
</div>
</div>
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">ZMQ (ZeroMQ)</div>
<div class="proto-card-desc">Publish-subscribe messaging from Bitcoin node. Instant notifications for new blocks and transactions.</div>
<div class="proto-card-layman">A broadcasting system. When a new Bitcoin block is found, Bitcoin instantly shouts it out and everyone listening hears immediately.</div>
<div class="proto-details">
<b>Endpoints:</b><br>
&bull; <code>tcp://bitcoin-knots:28332</code> &mdash; New block hashes (hashblock)<br>
&bull; <code>tcp://bitcoin-knots:28333</code> &mdash; New raw transactions (rawtx)<br>
<b>Pattern:</b> PUB/SUB (publisher/subscriber)<br>
<b>Subscribers:</b> LND, Mempool, ElectrumX
</div>
</div>
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">Tor (SOCKS5 + Hidden Services)</div>
<div class="proto-card-desc">Privacy layer. Routes Bitcoin P2P through onion routing, exposes services as .onion addresses.</div>
<div class="proto-card-layman">Like sending a letter through 3 random post offices so nobody knows where it came from. Also lets people reach your node without knowing your real IP.</div>
<div class="proto-details">
<b>SOCKS5 proxy:</b> <code>127.0.0.1:9050</code><br>
<b>Container access:</b> <code>host.containers.internal:9050</code><br>
<b>Hidden services:</b> Web UI, LND, BTCPay, Mempool, Fedimint<br>
<b>Managed by:</b> System Tor daemon + <code>tor-helper.sh</code> (privileged helper)<br>
<b>Used by:</b> Bitcoin P2P, LND P2P, BTCPay invoices
</div>
</div>
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">Nostr (NIP-01)</div>
<div class="proto-card-desc">Decentralized social protocol. WebSocket-based relay communication for events (posts, follows, messages).</div>
<div class="proto-card-layman">A social media protocol where no company controls the network. Your posts live on relays, and you own your identity with a cryptographic key.</div>
<div class="proto-details">
<b>Transport:</b> WebSocket (WSS)<br>
<b>Format:</b> JSON events signed with secp256k1 keys<br>
<b>Relay:</b> IndeedHub relay (local), configurable external relays<br>
<b>Integration:</b> <code>nostr-provider.js</code> injected into all app iframes<br>
<b>Identity:</b> DID-based, linked to node Ed25519 keypair
</div>
</div>
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">DWN (Decentralized Web Node)</div>
<div class="proto-card-desc">W3C protocol for storing encrypted data and messages in a decentralized way. Identity-linked storage.</div>
<div class="proto-card-layman">A personal data vault. Apps can store data here that only you control. Like a safety deposit box that follows you across the internet.</div>
<div class="proto-details">
<b>Endpoint:</b> <code>/dwn</code> (proxied through nginx)<br>
<b>Auth:</b> Per-record DID-based permissions<br>
<b>Reachable via:</b> Tor hidden service<br>
<b>Used for:</b> Encrypted backups, cross-node messaging, app data sync
</div>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- WEB5 -->
<!-- ============================================================ -->
<div class="section" id="sec-web5">
<div class="stats">
<div class="stat"><div class="stat-value">2</div><div class="stat-label">DID Methods</div></div>
<div class="stat"><div class="stat-value">26+</div><div class="stat-label">Identity RPCs</div></div>
<div class="stat"><div class="stat-value">W3C 2.0</div><div class="stat-label">VC Spec</div></div>
<div class="stat"><div class="stat-value">Ed25519</div><div class="stat-label">Primary Key</div></div>
<div class="stat"><div class="stat-value">DWN</div><div class="stat-label">Data Store</div></div>
<div class="stat"><div class="stat-value">Dual Key</div><div class="stat-label">Ed25519 + secp256k1</div></div>
</div>
<div class="layer-stack">
<div class="layer l-ui" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Applications</span>
<span class="layer-tag badge-purple">Layer 5 &middot; UI</span>
</div>
<div class="layer-desc">Vue.js views for identity management, credential issuance, DWN dashboard, and quick actions</div>
<div class="layer-layman">The screens where you manage your digital identity, issue credentials, and control your personal data store.</div>
<div class="layer-details">
<h4>Key Views</h4>
<ul>
<li><code>Web5Identities.vue</code> &mdash; Create/manage identities (Personal, Business, Anonymous purposes)</li>
<li><code>Web5CredentialsSummary.vue</code> &mdash; View issued/held credentials with status badges</li>
<li><code>Web5DWN.vue</code> &mdash; DWN status, protocol registration, message browser</li>
<li><code>Web5QuickActions.vue</code> &mdash; Copy DID, publish to DHT, trigger sync</li>
</ul>
<h4>Data Types (TypeScript)</h4>
<ul>
<li><code>ManagedIdentity</code> &mdash; id, name, purpose, did, pubkey, nostr_pubkey, profile</li>
<li><code>VCData</code> &mdash; id, issuer, subject, type, claims, status (active/revoked/expired)</li>
<li><code>DwnStatusData</code> &mdash; running, sync_status, message_count, registered_protocols</li>
</ul>
</div>
</div>
<div class="layer l-svc" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Verifiable Credentials (W3C VC 2.0)</span>
<span class="layer-tag badge-blue">Layer 4 &middot; Trust</span>
</div>
<div class="layer-desc">Issue, verify, revoke, and present credentials with Ed25519Signature2020 proofs &mdash; W3C VC Data Model 2.0</div>
<div class="layer-layman">Digital certificates that prove things about you &mdash; signed by one identity, held by another, verified by anyone. Like a digitally signed diploma.</div>
<div class="layer-details">
<h4>Three-Party Model</h4>
<ul>
<li><b>Issuer:</b> Creates and signs the credential (any managed identity)</li>
<li><b>Holder:</b> Stores credentials, creates Verifiable Presentations</li>
<li><b>Verifier:</b> Checks signature + expiration + revocation status</li>
</ul>
<h4>Credential Structure</h4>
<ul>
<li><code>@context</code>: W3C Credentials v2 + Ed25519 signature suite</li>
<li><code>type</code>: ["VerifiableCredential", "CustomType"]</li>
<li><code>issuer</code>: did:key or did:dht</li>
<li><code>credentialSubject</code>: { id: did, claims: {...} }</li>
<li><code>proof</code>: Ed25519Signature2020 with verification method reference</li>
<li><code>credentialStatus</code>: CredentialStatusList2021 for revocation</li>
</ul>
<h4>Verifiable Presentations</h4>
<ul>
<li>Bundle one or more VCs with holder's own signature</li>
<li>Proof purpose: <code>authentication</code> (vs. <code>assertionMethod</code> for VCs)</li>
<li>Selective disclosure &mdash; present only relevant credentials</li>
</ul>
<h4>RPC Methods</h4>
<ul>
<li><code>identity.issue-credential</code> &mdash; Issue from any managed identity</li>
<li><code>identity.verify-credential</code> &mdash; Verify by credential ID</li>
<li><code>identity.list-credentials</code> &mdash; List with optional filtering</li>
</ul>
<h4>Storage</h4>
<ul>
<li><code>/var/lib/archipelago/credentials/store.json</code></li>
</ul>
</div>
</div>
<div class="layer l-cnt" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Decentralized Web Node (DWN)</span>
<span class="layer-tag badge-green">Layer 3 &middot; Storage</span>
</div>
<div class="layer-desc">Personal data store with protocol-governed records, peer sync over Tor, and DID-based authorization</div>
<div class="layer-layman">Your personal database that YOU own. Apps ask permission to read/write data. Syncs with trusted peers automatically over Tor.</div>
<div class="layer-details">
<h4>Records Interface</h4>
<ul>
<li><code>Records.Write</code> &mdash; Store a message (UUID-based record_id)</li>
<li><code>Records.Read</code> &mdash; Retrieve by record_id</li>
<li><code>Records.Query</code> &mdash; Filter by protocol, schema, author, date range</li>
<li><code>Records.Delete</code> &mdash; Remove record</li>
</ul>
<h4>Protocol Definitions</h4>
<ul>
<li>Declarative rule sets governing data structure and access permissions</li>
<li><b>types:</b> Define allowed dataFormats and optional schema URIs</li>
<li><b>structure:</b> Hierarchical &mdash; records can have child records (post &rarr; comment)</li>
<li><b>$actions:</b> Who can create/read/update/delete (anyone, author, recipient)</li>
<li>Registered via <code>dwn.register-protocol</code> RPC, enforced automatically</li>
</ul>
<h4>Peer Sync</h4>
<ul>
<li>Bidirectional sync with trusted peers over Tor SOCKS5 proxy (<code>127.0.0.1:9050</code>)</li>
<li>Deduplication by record_id, batched (200 messages per sync)</li>
<li>30s per-peer timeout, 90s total timeout</li>
<li>State persisted to <code>/var/lib/archipelago/dwn/sync_state.json</code></li>
<li>Triggered manually or via background task</li>
</ul>
<h4>HTTP API</h4>
<ul>
<li>Endpoint: <code>POST /dwn</code> (proxied through nginx)</li>
<li>Reachable remotely via Tor hidden service</li>
</ul>
<h4>RPC Methods (8)</h4>
<ul>
<li><code>dwn.status</code> &mdash; Running state, sync status, message count</li>
<li><code>dwn.sync</code> &mdash; Trigger background sync with trusted peers</li>
<li><code>dwn.register-protocol</code> / <code>dwn.list-protocols</code> / <code>dwn.remove-protocol</code></li>
<li><code>dwn.write-message</code> / <code>dwn.query-messages</code> / <code>dwn.read-message</code> / <code>dwn.delete-message</code></li>
</ul>
<h4>Storage</h4>
<ul>
<li>Messages: <code>/var/lib/archipelago/dwn/messages/{record_id}.json</code></li>
<li>Protocols: <code>/var/lib/archipelago/dwn/protocols/{protocol_uri}.json</code></li>
</ul>
</div>
</div>
<div class="layer l-net" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Decentralized Identifiers (DIDs)</span>
<span class="layer-tag badge-cyan">Layer 2 &middot; Identity</span>
</div>
<div class="layer-desc">W3C DID Core 1.0 &mdash; did:key (primary, offline-capable) and did:dht (discoverability via BitTorrent Mainline DHT)</div>
<div class="layer-layman">Your self-sovereign digital identity. No company issues it, no platform controls it. You prove who you are with cryptographic keys.</div>
<div class="layer-details">
<h4>did:key (Primary Method)</h4>
<ul>
<li>Self-contained &mdash; no external resolution, works fully offline</li>
<li>Format: <code>did:key:z6Mk...</code> (multicodec Ed25519 in base58btc)</li>
<li>Instant, zero-cost, no network dependency</li>
<li>Used for: VCs, federation trust, backup encryption, DWN signing</li>
<li>Cannot be rotated &mdash; key <i>is</i> the identifier</li>
</ul>
<h4>did:dht (Discovery Method)</h4>
<ul>
<li>Publishes DID Document to BitTorrent Mainline DHT via BEP-44 signed mutable items</li>
<li>Format: <code>did:dht:z...</code> (z-base-32 encoded Ed25519 pubkey)</li>
<li>Globally discoverable without any centralized registry</li>
<li>DID Document encoded as DNS Resource Records in DNS packet</li>
<li>Supports key rotation (increment sequence number, republish)</li>
<li>1-hour TTL cache for performance</li>
<li>Replaced did:ion (Bitcoin-anchored) &mdash; simpler, no full node required</li>
</ul>
<h4>DID Document (W3C Core 1.0)</h4>
<ul>
<li><code>verificationMethod</code>: Ed25519VerificationKey2020 + derived X25519KeyAgreementKey2020</li>
<li><code>authentication</code>, <code>assertionMethod</code>, <code>capabilityInvocation</code>, <code>capabilityDelegation</code></li>
<li><code>keyAgreement</code>: X25519 (derived from Ed25519 via Curve25519)</li>
<li>Optional <code>EcdsaSecp256k1VerificationKey2019</code> for Nostr interop</li>
<li>Service endpoints: DWN URL, Nostr relay list</li>
</ul>
<h4>Multi-Identity Manager</h4>
<ul>
<li>Users create multiple identities with purpose tags: Personal, Business, Anonymous</li>
<li>One default identity (marked with star in UI)</li>
<li>Each identity: Ed25519 key + optional Nostr secp256k1 key + optional NIP-01 profile</li>
<li>Stored as JSON in <code>/var/lib/archipelago/identities/{id}.json</code></li>
</ul>
<h4>Identity RPC Methods (26+)</h4>
<ul>
<li><code>identity.create</code> / <code>.list</code> / <code>.get</code> / <code>.delete</code> / <code>.set-default</code></li>
<li><code>identity.sign</code> / <code>.verify</code> &mdash; Ed25519 message signing</li>
<li><code>identity.resolve-did</code> / <code>.verify-did-document</code></li>
<li><code>identity.create-dht-did</code> / <code>.resolve-dht-did</code> / <code>.refresh-dht-did</code></li>
<li><code>identity.create-nostr-key</code> / <code>.nostr-sign</code></li>
<li><code>identity.nostr-encrypt-nip04</code> / <code>.nostr-decrypt-nip04</code></li>
<li><code>identity.nostr-encrypt-nip44</code> / <code>.nostr-decrypt-nip44</code></li>
<li><code>identity.update-profile</code> / <code>.resolve-remote-did</code></li>
</ul>
</div>
</div>
<div class="layer l-enc" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Cryptographic Keys</span>
<span class="layer-tag badge-purple">Layer 1 &middot; Foundation</span>
</div>
<div class="layer-desc">Dual key architecture &mdash; Ed25519 for Web5/DIDs + secp256k1 for Bitcoin/Nostr, both derived from BIP-39 master seed</div>
<div class="layer-layman">Two types of cryptographic keys derived from one master seed. One for identity (Web5), one for money and social (Bitcoin/Nostr).</div>
<div class="layer-details">
<h4>Ed25519 (Web5 &amp; Identity)</h4>
<ul>
<li>W3C DIDs, Verifiable Credentials (Ed25519Signature2020)</li>
<li>DWN message signing and authorization</li>
<li>Federation peer authentication and trust</li>
<li>Backup encryption (via derived X25519 key agreement)</li>
<li>Storage: <code>/var/lib/archipelago/identity/node_key</code> (32 bytes raw)</li>
</ul>
<h4>secp256k1 (Bitcoin &amp; Nostr)</h4>
<ul>
<li>Nostr event signing (NIP-01), encrypted DMs (NIP-04, NIP-44)</li>
<li>Lightning Network node identity</li>
<li>Social presence and discovery (NIP-05, kind 30078)</li>
<li>Format: hex pubkey + Nostr npub (NIP-19 bech32)</li>
</ul>
<h4>Key Derivation</h4>
<ul>
<li>Single BIP-39 master seed (12 or 24 word mnemonic)</li>
<li>Deterministic derivation of both Ed25519 and secp256k1 keys</li>
<li>All keys recoverable from seed phrase alone</li>
<li>Seed generated at first boot, stored on LUKS-encrypted partition</li>
</ul>
<h4>Rust Dependencies</h4>
<ul>
<li><code>ed25519-dalek 2.2.0</code> &mdash; Ed25519 signatures</li>
<li><code>curve25519-dalek 4.1.3</code> &mdash; X25519 key agreement (Ed25519 &rarr; X25519 conversion)</li>
<li><code>nostr-sdk 0.44</code> &mdash; secp256k1 signing, NIP-04/44 encryption</li>
<li><code>mainline 2</code> &mdash; BitTorrent Mainline DHT client (did:dht)</li>
<li><code>zbase32 0.1</code> &mdash; z-base-32 encoding for DID identifiers</li>
</ul>
</div>
</div>
</div>
<h3 class="subsection-title">Specification Status</h3>
<p style="font-size: 12px; color: #8b8fa3; margin-bottom: 16px; line-height: 1.5;">
Web5 was initiated by TBD (Block/Jack Dorsey) and shut down November 2024. Open-source components were donated to the <b style="color:#c4c8e0;">Decentralized Identity Foundation (DIF)</b>.
The W3C specs (DIDs, VCs) are independent standards with broad industry adoption. Archipelago implements these W3C standards directly with a custom DWN &mdash; not dependent on TBD's SDK.
</p>
<table class="cmp-table">
<tr><th>Component</th><th>Spec</th><th>Status</th><th>Archipelago</th></tr>
<tr><td class="cmp-label">DID Core 1.0</td><td>W3C Recommendation</td><td><span class="card-badge badge-green">Stable</span></td><td>did:key + did:dht, full DID Document generation</td></tr>
<tr><td class="cmp-label">VC Data Model 2.0</td><td>W3C Recommendation (May 2025)</td><td><span class="card-badge badge-green">Stable</span></td><td>Issue/verify/revoke with Ed25519Signature2020</td></tr>
<tr><td class="cmp-label">DWN</td><td>DIF Draft</td><td><span class="card-badge badge-amber">Draft</span></td><td>Custom Records interface, protocol management, Tor sync</td></tr>
<tr><td class="cmp-label">did:dht</td><td>Near v1.0</td><td><span class="card-badge badge-amber">Active</span></td><td>Mainline DHT publishing via <code>mainline</code> crate</td></tr>
<tr><td class="cmp-label">did:ion (Sidetree)</td><td>DIF 1.0</td><td><span class="card-badge badge-red">Abandoned</span></td><td>Not implemented &mdash; requires Bitcoin + IPFS full nodes</td></tr>
<tr><td class="cmp-label">Presentation Exchange 2.0</td><td>DIF Ratified</td><td><span class="card-badge badge-green">Stable</span></td><td>Verifiable Presentations with holder proof</td></tr>
</table>
<h3 class="subsection-title">Architecture Decision Records</h3>
<table class="cmp-table">
<tr><th>ADR</th><th>Decision</th><th>Rationale</th></tr>
<tr><td class="cmp-label">ADR-002</td><td>did:key as primary DID method</td><td>Self-contained, offline-capable, zero-cost, aligns with sovereignty</td></tr>
<tr><td class="cmp-label">ADR-008</td><td>Dual key architecture (Ed25519 + secp256k1)</td><td>Ed25519 for W3C/Web5, secp256k1 for Bitcoin/Lightning/Nostr ecosystems</td></tr>
<tr><td class="cmp-label">ADR-011</td><td>Custom DWN, not full W3C spec compliance</td><td>TBD shut down, DWN spec stalled. Federation + Nostr relays prioritized for peer sync</td></tr>
</table>
</div>
<!-- ============================================================ -->
<!-- BOOT SEQUENCE -->
<!-- ============================================================ -->
<div class="section" id="sec-boot">
<div style="margin-bottom: 20px;">
<div class="boot-step">
<div class="boot-num" style="background:rgba(239,68,68,0.2); color:#fca5a5;">1</div>
<div class="boot-content">
<div class="boot-title">BIOS / UEFI &rarr; GRUB Bootloader</div>
<div class="boot-desc">Firmware loads GRUB from EFI partition or BIOS boot sector. GRUB loads the Linux kernel.</div>
<div class="boot-layman">The computer turns on and finds the operating system to start. Works on both old and new machines.</div>
<div class="boot-detail">GRUB installed for both UEFI (EFI partition) and legacy BIOS (1MB boot sector) &mdash; dual-boot compatible</div>
</div>
</div>
<div class="boot-step">
<div class="boot-num" style="background:rgba(168,85,247,0.2); color:#d8b4fe;">2</div>
<div class="boot-content">
<div class="boot-title">LUKS Unlock &rarr; Mount Encrypted Partition</div>
<div class="boot-desc">Cryptsetup opens the LUKS2 volume using <code>/root/.luks-archipelago.key</code> and mounts it at <code>/var/lib/archipelago</code>.</div>
<div class="boot-layman">The encrypted safe is unlocked automatically (no password prompt). All your data becomes accessible.</div>
<div class="boot-detail">Configured in /etc/crypttab for automatic boot-time unlock</div>
</div>
</div>
<div class="boot-step">
<div class="boot-num" style="background:rgba(245,158,11,0.2); color:#fcd34d;">3</div>
<div class="boot-content">
<div class="boot-title">systemd Starts &rarr; Network Online</div>
<div class="boot-desc">systemd boots Debian, starts networking, reaches <code>network-online.target</code>. Tor and Tailscale start.</div>
<div class="boot-layman">The operating system finishes starting up and connects to the internet.</div>
</div>
</div>
<div class="boot-step">
<div class="boot-num" style="background:rgba(99,102,241,0.2); color:#a5b4fc;">4</div>
<div class="boot-content">
<div class="boot-title">Archipelago Backend Starts</div>
<div class="boot-desc"><code>archipelago.service</code> launches the Rust binary. Runs crash recovery, starts container state snapshots, initializes JSON-RPC server on :5678.</div>
<div class="boot-layman">The brain of the system wakes up. If there was a crash last time, it automatically recovers.</div>
<div class="boot-detail">Type=notify, WatchdogSec=300s, MemoryMax=4G</div>
</div>
</div>
<div class="boot-step">
<div class="boot-num" style="background:rgba(16,185,129,0.2); color:#6ee7b7;">5</div>
<div class="boot-content">
<div class="boot-title">First Boot: Container Creation</div>
<div class="boot-desc"><code>first-boot-containers.sh</code> runs once (guarded by marker file). Creates all containers in tier order: databases &rarr; Bitcoin &rarr; services &rarr; apps.</div>
<div class="boot-layman">On first startup only: installs all the apps. Databases first, then Bitcoin, then everything else. Takes 5-15 minutes.</div>
<div class="boot-detail">ConditionPathExists=!/var/lib/archipelago/.first-boot-containers-done &middot; Timeout: 900s</div>
</div>
</div>
<div class="boot-step">
<div class="boot-num" style="background:rgba(6,182,212,0.2); color:#67e8f9;">6</div>
<div class="boot-content">
<div class="boot-title">Nginx Ready &rarr; Web UI Accessible</div>
<div class="boot-desc">Nginx serves the Vue.js SPA on :80/:443. Backend health check at <code>/health</code> passes.</div>
<div class="boot-layman">The website is now live. Open a browser and go to the machine's IP address to see the dashboard.</div>
</div>
</div>
<div class="boot-step">
<div class="boot-num" style="background:rgba(251,146,60,0.2); color:#fdba74;">7</div>
<div class="boot-content">
<div class="boot-title">Kiosk Starts (if enabled)</div>
<div class="boot-desc"><code>archipelago-kiosk.service</code> waits for <code>/health</code> endpoint (up to 30s), then starts X11 + Chromium on VT7.</div>
<div class="boot-layman">If a monitor is plugged in, the dashboard appears fullscreen automatically. No login needed for the local screen.</div>
<div class="boot-detail">Polls /health 15 times at 2s intervals before launching Chromium</div>
</div>
</div>
<div class="boot-step">
<div class="boot-num" style="background:rgba(139,92,246,0.2); color:#c4b5fd;">8</div>
<div class="boot-content">
<div class="boot-title">Background Services Start</div>
<div class="boot-desc">Timers activate: container doctor (health repair), reconciler (spec enforcement), self-update. Tor helper watches for service config changes.</div>
<div class="boot-layman">Maintenance robots start working in the background. They check on apps, fix broken ones, and keep everything updated.</div>
<div class="boot-detail">archipelago-doctor.timer, archipelago-reconcile.timer, archipelago-update.timer, archipelago-tor-helper.path</div>
</div>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- SECURITY -->
<!-- ============================================================ -->
<div class="section" id="sec-security">
<div class="sec-grid">
<div class="sec-card">
<div class="sec-title">LUKS2 Full-Disk Encryption</div>
<div class="sec-desc">All user data on an encrypted partition. Auto-detects AES-NI for hardware acceleration, falls back to ChaCha20-Adiantum. Key derived with Argon2id (GPU-resistant).</div>
<div class="sec-layman">If someone physically steals your hard drive, all they get is encrypted noise. The data is useless without the key.</div>
</div>
<div class="sec-card">
<div class="sec-title">Rootless Containers</div>
<div class="sec-desc">All containers run as unprivileged user archipelago (UID 1000). Even a compromised container cannot escalate to root. UID remapping isolates container users from host users.</div>
<div class="sec-layman">Apps run in sealed boxes without admin access. A hacked app can't take over the whole machine.</div>
</div>
<div class="sec-card">
<div class="sec-title">Capability Dropping</div>
<div class="sec-desc"><code>--cap-drop=ALL</code> then add only specific capabilities needed. <code>--security-opt=no-new-privileges</code> prevents privilege escalation inside containers.</div>
<div class="sec-layman">Each app only gets the exact permissions it needs, nothing more. Like giving a valet driver only the car key, not your house keys.</div>
</div>
<div class="sec-card">
<div class="sec-title">RBAC (Role-Based Access Control)</div>
<div class="sec-desc">Backend uses explicit method allowlists per role. No prefix matching &mdash; each RPC method must be explicitly permitted. Session cookies are HttpOnly, SameSite=Lax.</div>
<div class="sec-layman">Different users have different permissions. A viewer can't install apps, and nobody can run commands that aren't on the approved list.</div>
</div>
<div class="sec-card">
<div class="sec-title">Rate Limiting</div>
<div class="sec-desc">Nginx rate limits: auth endpoints (3/s), RPC (20/s), P2P (10/s). Prevents brute-force attacks and API abuse.</div>
<div class="sec-layman">If someone tries to guess your password by trying thousands of combinations, they get locked out after a few attempts per second.</div>
</div>
<div class="sec-card">
<div class="sec-title">Tor Privacy</div>
<div class="sec-desc">Bitcoin P2P routes through Tor with stream isolation. Hidden services expose node without revealing IP. LND uses Tor for Lightning P2P.</div>
<div class="sec-layman">Your Bitcoin node connects through the Tor privacy network. Nobody can see your real IP address or location.</div>
</div>
<div class="sec-card">
<div class="sec-title">Credential Management</div>
<div class="sec-desc">Secrets auto-generated at first boot (CSPRNG). Stored in <code>/var/lib/archipelago/secrets/</code> (mode 700). Bitcoin RPC uses HMAC-SHA256 auth hashes, never plaintext.</div>
<div class="sec-layman">Passwords are randomly generated and stored securely. They never appear in config files as readable text.</div>
</div>
<div class="sec-card">
<div class="sec-title">Memory Limits &amp; Health Checks</div>
<div class="sec-desc">Every container has a memory limit (128MB&ndash;4GB). Health checks auto-restart failed containers. Backend has systemd watchdog (300s).</div>
<div class="sec-layman">Runaway apps can't eat all the memory. Crashed apps restart automatically. The system self-heals.</div>
</div>
<div class="sec-card">
<div class="sec-title">Security Headers</div>
<div class="sec-desc">CSP (Content Security Policy), HSTS, X-Frame-Options: SAMEORIGIN, X-Content-Type-Options: nosniff, strict Referrer-Policy, disabled camera/mic/geolocation.</div>
<div class="sec-layman">The web UI tells browsers to follow strict security rules &mdash; no loading scripts from unknown sites, no accessing your camera.</div>
</div>
<div class="sec-card">
<div class="sec-title">Systemd Hardening</div>
<div class="sec-desc"><code>ProtectSystem=strict</code>, <code>MemoryDenyWriteExecute</code>, <code>RestrictRealtime</code>, <code>RestrictAddressFamilies</code>. Backend can only write to approved paths.</div>
<div class="sec-layman">The operating system restricts what the backend can do. It can only touch the files it needs to, nothing else on the system.</div>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- DATA PATHS -->
<!-- ============================================================ -->
<div class="section" id="sec-data">
<div class="path-tree"><span class="dir">/var/lib/archipelago/</span> <span class="desc">&larr; LUKS2 encrypted partition</span>
<span class="dir"> ├── bitcoin/</span> <span class="desc">Bitcoin blockchain data (~500GB full, ~550MB pruned)</span>
<span class="dir"> ├── lnd/</span> <span class="desc">Lightning wallet, channels, macaroons, TLS cert</span>
<span class="dir"> ├── electrumx/</span> <span class="desc">Address index database</span>
<span class="dir"> ├── postgres-btcpay/</span> <span class="desc">BTCPay PostgreSQL data</span>
<span class="dir"> ├── mysql-mempool/</span> <span class="desc">Mempool MariaDB data</span>
<span class="dir"> ├── mempool/</span> <span class="desc">Mempool backend cache</span>
<span class="dir"> ├── btcpay/</span> <span class="desc">BTCPay server data, plugins</span>
<span class="dir"> ├── nbxplorer/</span> <span class="desc">NBXplorer blockchain scan state</span>
<span class="dir"> ├── fedimint/</span> <span class="desc">Federation data, consensus state</span>
<span class="dir"> ├── fedimint-gateway/</span> <span class="desc">Gateway keys and routing table</span>
<span class="dir"> ├── home-assistant/</span> <span class="desc">Smart home config, automations, database</span>
<span class="dir"> ├── grafana/</span> <span class="desc">Dashboards, datasources, alerting rules</span>
<span class="dir"> ├── uptime-kuma/</span> <span class="desc">Monitor definitions, status history</span>
<span class="dir"> ├── jellyfin/</span> <span class="desc">Media library metadata, transcoding cache</span>
<span class="dir"> ├── photoprism/</span> <span class="desc">Photo index, thumbnails, AI models</span>
<span class="dir"> ├── ollama/</span> <span class="desc">Downloaded LLM models (can be multi-GB)</span>
<span class="dir"> ├── vaultwarden/</span> <span class="desc">Encrypted password vault database</span>
<span class="dir"> ├── nextcloud/</span> <span class="desc">Files, calendar, contacts, config</span>
<span class="dir"> ├── searxng/</span> <span class="desc">Search engine settings</span>
<span class="dir"> ├── filebrowser/</span> <span class="desc">Served files (user uploads)</span>
<span class="dir"> ├── filebrowser-data/</span> <span class="desc">FileBrowser internal database</span>
<span class="dir"> ├── nginx-proxy-manager/</span> <span class="desc">Proxy rules, Let's Encrypt certificates</span>
<span class="dir"> ├── portainer/</span> <span class="desc">Portainer config and database</span>
<span class="dir"> ├── tailscale/</span> <span class="desc">VPN state, node identity</span>
<span class="dir"> ├── secrets/</span> <span class="desc">RPC passwords, DB passwords (mode 700)</span>
<span class="dir"> ├── identity/</span> <span class="desc">Node Ed25519 keypair (DID identity)</span>
<span class="dir"> ├── identities/</span> <span class="desc">User DIDs</span>
<span class="dir"> ├── tor-config/</span> <span class="desc">Tor service definitions (backend-managed)</span>
<span class="dir"> ├── tor-hostnames/</span> <span class="desc">.onion addresses (synced from /var/lib/tor)</span>
<span class="dir"> └── .first-boot-containers-done</span> <span class="desc">Marker: first boot completed</span>
<span class="dir">/opt/archipelago/</span> <span class="desc">&larr; Unencrypted (on root partition)</span>
<span class="dir"> ├── web-ui/</span> <span class="desc">Vue.js SPA (static files served by nginx)</span>
<span class="dir"> ├── scripts/</span> <span class="desc">Deploy, container, and maintenance scripts</span>
<span class="dir"> └── image-versions.sh</span> <span class="desc">Pinned container image versions</span>
<span class="dir">/usr/local/bin/archipelago</span> <span class="desc">Rust backend binary</span>
<span class="dir">/etc/nginx/</span> <span class="desc">Nginx config (reverse proxy rules)</span>
<span class="dir">/etc/tor/torrc</span> <span class="desc">Tor daemon configuration</span></div>
</div>
<!-- ============================================================ -->
<!-- GLOSSARY -->
<!-- ============================================================ -->
<div class="section" id="sec-glossary">
<div class="glossary">
<div class="glossary-item">
<div class="glossary-term">Podman</div>
<div class="glossary-tech">Daemonless container engine (OCI-compatible, Docker alternative)</div>
<div class="glossary-plain">A tool that runs apps in isolated sandboxes. Like Docker but doesn't need a background service running as root.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">Rootless</div>
<div class="glossary-tech">Containers run entirely in user namespace, no root privileges required</div>
<div class="glossary-plain">The sandboxes run without admin access. Even if someone breaks into one, they can't take over the system.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">LUKS2</div>
<div class="glossary-tech">Linux Unified Key Setup v2 &mdash; dm-crypt disk encryption with Argon2 KDF</div>
<div class="glossary-plain">Industry-standard disk encryption for Linux. Scrambles the entire partition so data is unreadable without the key.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">Argon2id</div>
<div class="glossary-tech">Memory-hard password hashing function resistant to GPU/ASIC brute-force</div>
<div class="glossary-plain">A way to protect passwords that requires lots of memory to crack, making it extremely expensive to brute-force even with specialized hardware.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">Nginx</div>
<div class="glossary-tech">High-performance HTTP reverse proxy and web server</div>
<div class="glossary-plain">The front door of the system. All web traffic goes through nginx, which directs each request to the right app.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">Reverse Proxy</div>
<div class="glossary-tech">Server that forwards client requests to backend services based on URL path or hostname</div>
<div class="glossary-plain">A traffic cop for web requests. You visit one address, and the proxy routes you to the right app behind the scenes.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">JSON-RPC</div>
<div class="glossary-tech">Remote procedure call protocol using JSON over HTTP/TCP</div>
<div class="glossary-plain">A simple way for one program to ask another to do something. Send a JSON message, get a JSON reply.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">WebSocket</div>
<div class="glossary-tech">Full-duplex TCP communication channel over HTTP upgrade</div>
<div class="glossary-plain">A persistent connection between browser and server. Instead of repeatedly asking "anything new?", the server pushes updates instantly.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">gRPC</div>
<div class="glossary-tech">Google's high-performance RPC framework using Protocol Buffers over HTTP/2</div>
<div class="glossary-plain">A fast, structured way for programs to communicate. Used by LND because it handles many Lightning operations efficiently.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">ZMQ (ZeroMQ)</div>
<div class="glossary-tech">Asynchronous messaging library for pub/sub and push/pull patterns</div>
<div class="glossary-plain">A broadcasting system. Bitcoin publishes "new block!" and every subscribed app hears it instantly.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">Tor</div>
<div class="glossary-tech">Onion routing network for anonymous communication via encrypted relay circuits</div>
<div class="glossary-plain">A privacy network that bounces your traffic through multiple servers so nobody can trace it back to you.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">Hidden Service (.onion)</div>
<div class="glossary-tech">Tor service accessible via .onion address without revealing server IP</div>
<div class="glossary-plain">A way to make your node reachable on the internet without revealing your IP address or location.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">Tailscale</div>
<div class="glossary-tech">WireGuard-based mesh VPN with NAT traversal and SSO integration</div>
<div class="glossary-plain">A private tunnel to your node from anywhere. Like a VPN but easier &mdash; install the app on your phone, and you can access the node from a coffee shop.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">Macaroon</div>
<div class="glossary-tech">Bearer token with embedded caveats (permissions) used by LND for API auth</div>
<div class="glossary-plain">A special key for LND that says exactly what you're allowed to do. A "read-only" macaroon can check balance but can't send money.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">CSRF Token</div>
<div class="glossary-tech">Cross-Site Request Forgery prevention token sent in cookie + header</div>
<div class="glossary-plain">A secret code that proves your browser request is genuine and not a trick from a malicious website.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">DID (Decentralized Identifier)</div>
<div class="glossary-tech">W3C standard for self-sovereign identity using cryptographic keypairs</div>
<div class="glossary-plain">Your digital identity that you own completely. Like a passport that no government issued &mdash; you prove who you are with math, not authority.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">Nostr</div>
<div class="glossary-tech">Notes and Other Stuff Transmitted by Relays &mdash; decentralized social protocol</div>
<div class="glossary-plain">A social media protocol where you own your identity. No company can ban you because your account is just a cryptographic key.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">Lightning Network</div>
<div class="glossary-tech">Bitcoin Layer 2 payment channel network for instant, low-fee transactions</div>
<div class="glossary-plain">A way to send Bitcoin instantly (milliseconds) for tiny fees. Works by opening "tabs" between nodes and settling on-chain later.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">Fedimint</div>
<div class="glossary-tech">Federated Bitcoin custody protocol with threshold signing and Chaumian e-cash</div>
<div class="glossary-plain">A community Bitcoin bank where a group of trusted guardians hold funds together. No single person can steal &mdash; you need a majority to sign.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">archy-net</div>
<div class="glossary-tech">Custom Podman bridge network with DNS resolution for Bitcoin-stack containers</div>
<div class="glossary-plain">A private network inside the box where Bitcoin apps can find each other by name. Like a local phone book for containers.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">Capability (CAP)</div>
<div class="glossary-tech">Fine-grained Linux privilege (e.g. CAP_NET_RAW, CAP_CHOWN) instead of full root</div>
<div class="glossary-plain">Instead of giving an app all admin powers, we give it only the specific abilities it needs. A file manager gets "change file ownership" but not "change network settings."</div>
</div>
<div class="glossary-item">
<div class="glossary-term">Systemd</div>
<div class="glossary-tech">Linux init system and service manager (PID 1)</div>
<div class="glossary-plain">The thing that starts everything when Linux boots. Manages all services, restarts crashed ones, and enforces resource limits.</div>
</div>
<div class="glossary-item">
<div class="glossary-term">RBAC</div>
<div class="glossary-tech">Role-Based Access Control &mdash; permissions assigned by user role, not individually</div>
<div class="glossary-plain">Different users get different permissions based on their role (admin, viewer, etc). Prevents regular users from doing dangerous things.</div>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- STARTOS: OVERVIEW -->
<!-- ============================================================ -->
<div class="section" id="sec-start9-overview">
<p style="font-size: 13px; color: #8b8fa3; margin-bottom: 16px;">Architecture analysis sourced from <code style="background:rgba(255,255,255,0.06); padding:1px 6px; border-radius:3px; font-family:'SF Mono',monospace; font-size:11px;">Start9Labs/start-os</code> on GitHub (master branch). Click any layer to expand.</p>
<div class="stats">
<div class="stat"><div class="stat-value">LXC</div><div class="stat-label">Container Runtime</div></div>
<div class="stat"><div class="stat-value">Rust</div><div class="stat-label">Backend (startd)</div></div>
<div class="stat"><div class="stat-value">Angular 21</div><div class="stat-label">Frontend</div></div>
<div class="stat"><div class="stat-value">S9PK v2</div><div class="stat-label">Package Format</div></div>
<div class="stat"><div class="stat-value">Optional</div><div class="stat-label">LUKS Encryption</div></div>
<div class="stat"><div class="stat-value">btrfs</div><div class="stat-label">Filesystem</div></div>
</div>
<div class="layer-stack">
<div class="layer l-ui"><div class="layer-header"><span class="layer-name">Angular 21 + Taiga UI 5</span><span class="layer-tag badge-purple">UI</span></div><div class="layer-desc">Three Angular apps: admin UI, setup wizard, VPN management. Patch-DB reactive sync via CBOR diffs over WebSocket.</div></div>
<div class="layer l-svc"><div class="layer-header"><span class="layer-name">Rust Backend (startd / startbox)</span><span class="layer-tag badge-blue">Service</span></div><div class="layer-desc">Single binary with 5 personalities (symlinks). Built-in reverse proxy (Axum), DNS (hickory-server), ACME, WireGuard, SOCKS5.</div></div>
<div class="layer l-cnt"><div class="layer-header"><span class="layer-name">LXC Containers</span><span class="layer-tag badge-green">Isolation</span></div><div class="layer-desc">Two-layer model: outer LXC per service (SquashFS + OverlayFS), inner subcontainers from S9PK images. JSON-RPC over Unix sockets.</div></div>
<div class="layer l-net"><div class="layer-header"><span class="layer-name">Network (built-in)</span><span class="layer-tag badge-cyan">Network</span></div><div class="layer-desc">VHostController reverse proxy, hickory-server DNS, ACME TLS, WireGuard tunnels, SOCKS5 at 10.0.3.1:1080. No nginx/caddy.</div></div>
<div class="layer l-enc"><div class="layer-header"><span class="layer-name">Optional LUKS on btrfs</span><span class="layer-tag badge-purple">Security</span></div><div class="layer-desc">User chooses encrypted or unencrypted during setup. LVM with btrfs for COW snapshots enabling safe app installs.</div></div>
<div class="layer l-os"><div class="layer-header"><span class="layer-name">Debian Bookworm</span><span class="layer-tag badge-amber">OS</span></div><div class="layer-desc">Same Debian 12 base. Targets x86_64, ARM64 (aarch64), and RISC-V (riscv64).</div></div>
</div>
</div>
<!-- ============================================================ -->
<!-- STARTOS: SYSTEM LAYERS -->
<!-- ============================================================ -->
<div class="section" id="sec-start9-layers">
<div class="layer-stack">
<div class="layer l-ui" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Web UI &mdash; Angular 21</span>
<span class="layer-tag badge-purple">Application</span>
</div>
<div class="layer-desc">Angular 21 + TypeScript + Taiga UI 5 components, served directly by the Rust backend (Axum)</div>
<div class="layer-layman">The dashboard you use in your browser. Built with Angular (Google's web framework), not served by a separate web server.</div>
<div class="layer-details">
<h4>Three Separate Angular Apps</h4>
<ul>
<li><code>projects/ui/</code> &mdash; Main admin interface</li>
<li><code>projects/setup-wizard/</code> &mdash; Initial setup flow</li>
<li><code>projects/start-tunnel/</code> &mdash; VPN management UI</li>
</ul>
<h4>State Management</h4>
<ul>
<li><b>Patch-DB:</b> Backend pushes CBOR diffs over WebSocket</li>
<li>Frontend applies diffs and notifies observers via <code>PatchDB.watch$()</code></li>
<li>Converted to Angular signals via <code>toSignal()</code></li>
<li>Reactive &mdash; UI updates automatically when backend state changes</li>
</ul>
<h4>Communication</h4>
<ul>
<li>JSON-RPC exclusively (not REST)</li>
<li><code>ApiService</code> abstract class with 100+ methods</li>
<li>i18n: 5 languages (en, es, de, fr, pl)</li>
</ul>
</div>
</div>
<div class="layer l-svc" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Rust Backend &mdash; startd (startbox)</span>
<span class="layer-tag badge-blue">Service</span>
</div>
<div class="layer-desc">Single Rust binary (multi-personality via symlinks: startd, start-cli, start-container, registrybox, tunnelbox)</div>
<div class="layer-layman">The brain of the system. One binary that does everything &mdash; serves the UI, manages containers, handles networking, runs the built-in reverse proxy.</div>
<div class="layer-details">
<h4>Key Components</h4>
<ul>
<li><b>Axum web server:</b> Serves UI + JSON-RPC API (no separate web server)</li>
<li><b>VHostController:</b> Built-in reverse proxy with TLS termination (no nginx/caddy)</li>
<li><b>Patch-DB:</b> Custom CBOR-encoded reactive database with diff-based WebSocket sync</li>
<li><b>LxcManager:</b> Container lifecycle (create, destroy, garbage collection)</li>
<li><b>NetController:</b> DNS (hickory-server), SOCKS5, ACME, WiFi, WireGuard, port forwarding</li>
<li><b>Service Actors:</b> Per-service state machines managing lifecycle</li>
</ul>
<h4>Binary Personalities (symlinks)</h4>
<ul>
<li><code>startd</code> &mdash; Main daemon</li>
<li><code>start-cli</code> &mdash; CLI interface</li>
<li><code>start-container</code> &mdash; Runs inside LXC containers, communicates with host</li>
<li><code>registrybox</code> &mdash; Package registry daemon</li>
<li><code>tunnelbox</code> &mdash; WireGuard VPN tunnel daemon</li>
</ul>
<h4>Key Dependencies</h4>
<ul>
<li>Async: Tokio &middot; Web: Axum 0.8 + Hyper 1.5 &middot; TLS: tokio-rustls 0.26 + OpenSSL (vendored)</li>
<li>DNS: hickory-server &middot; Crypto: blake3, ed25519, x25519-dalek, aes</li>
<li>TypeScript bindings: ts-rs (auto-generates TS types from Rust structs)</li>
</ul>
<h4>Systemd</h4>
<ul>
<li><code>startd.service</code>: Type=simple, Restart=always, RestartSec=3</li>
<li>LimitNOFILE=65536</li>
</ul>
</div>
</div>
<div class="layer l-cnt" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Container Layer &mdash; LXC</span>
<span class="layer-tag badge-green">Isolation</span>
</div>
<div class="layer-desc">Linux Containers (LXC) with two-layer model: outer LXC per service + inner subcontainers from S9PK images</div>
<div class="layer-layman">Each app gets its own sealed Linux environment. Unlike Docker, these are full system containers with their own init process.</div>
<div class="layer-details">
<h4>Two-Layer Container Model</h4>
<ul>
<li><b>Outer LXC container:</b> One per service. Created by Rust backend via lxc-create/destroy</li>
<li><b>Base rootfs:</b> SquashFS image (<code>/usr/lib/startos/container-runtime/rootfs.squashfs</code>) mounted as OverlayFS</li>
<li><b>Inner subcontainers:</b> Node.js container runtime inside each LXC can launch additional containers from S9PK-bundled images</li>
<li><b>Timeout:</b> 30-second container creation timeout</li>
</ul>
<h4>LXC Configuration</h4>
<ul>
<li>User namespaces: <code>lxc.idmap = u 0 100000 65536</code></li>
<li>AppArmor profile: generated with nesting allowed</li>
<li>Network: veth bridge on <code>lxcbr0</code> (10.0.3.x subnet, host at 10.0.3.1)</li>
<li>OverlayFS rootfs (base read-only squashfs, writes to overlay)</li>
<li>GPU passthrough support: /dev/dri, /dev/nvidia*, /dev/kfd</li>
</ul>
<h4>Communication</h4>
<ul>
<li>JSON-RPC over Unix domain sockets</li>
<li><code>/media/startos/rpc/service.sock</code> &mdash; Inbound (runtime listens)</li>
<li><code>/media/startos/rpc/host.sock</code> &mdash; Host callbacks (effects)</li>
</ul>
</div>
</div>
<div class="layer l-net" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Network Layer</span>
<span class="layer-tag badge-cyan">Network</span>
</div>
<div class="layer-desc">Built-in reverse proxy (Axum/Hyper), DNS (hickory-server), SOCKS5, ACME (Let's Encrypt), WireGuard tunnels</div>
<div class="layer-layman">No nginx or caddy &mdash; the Rust backend IS the web server, proxy, and DNS. Also manages VPN tunnels and encryption certificates.</div>
<div class="layer-details">
<h4>Built-in Reverse Proxy</h4>
<ul>
<li>VHostController in <code>core/src/net/vhost.rs</code> handles all HTTP routing</li>
<li>TLS termination via tokio-rustls with SNI-based routing</li>
<li>No external proxy software (no nginx, no caddy, no traefik)</li>
<li>Virtual hosting with per-service domain assignment</li>
</ul>
<h4>DNS</h4>
<ul>
<li>Built-in DNS server using <code>hickory-server</code> (formerly trust-dns)</li>
<li>Service discovery and resolution for containers</li>
<li>mDNS via <code>avahi-resolve-host-name</code> for <code>.local</code> domains</li>
</ul>
<h4>TLS / Certificates</h4>
<ul>
<li>Self-signed root CA per server (NIST P-256 via OpenSSL)</li>
<li>Built-in ACME client (<code>async-acme</code>) for Let's Encrypt with TLS-ALPN-01 challenge</li>
<li>Certificate store managed in Patch-DB</li>
</ul>
<h4>Connectivity</h4>
<ul>
<li><b>SOCKS5 proxy:</b> Built-in at <code>10.0.3.1:1080</code> for container outbound traffic</li>
<li><b>WireGuard:</b> First-class support via <code>wg-quick</code> + <code>x25519-dalek</code></li>
<li><b>Multi-gateway:</b> Supports multiple interfaces (Ethernet, WiFi, WireGuard) with separate domain configs</li>
<li><b>Port forwarding:</b> iptables-based via <code>InterfacePortForwardController</code></li>
</ul>
<h4>Tor (Status: Removed in v0.4)</h4>
<ul>
<li>Architecture doc mentions "Tor via Arti" but Arti is absent from current Cargo.toml</li>
<li>Previous versions (0.3.x) used the C Tor daemon for hidden services</li>
<li>Likely planned for re-integration but not yet implemented in the 0.4 rewrite</li>
</ul>
</div>
</div>
<div class="layer l-enc" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Encryption &mdash; Optional LUKS on btrfs</span>
<span class="layer-tag badge-purple">Security</span>
</div>
<div class="layer-desc">Optional LUKS encryption on LVM volumes, btrfs filesystem with COW snapshots for safe installs</div>
<div class="layer-layman">Disk encryption is optional (you choose during setup). Uses btrfs which can make instant copies of data for safe app updates.</div>
<div class="layer-details">
<h4>Encryption (Optional)</h4>
<ul>
<li>User chooses encrypted or unencrypted during setup</li>
<li>LVM volume groups: <code>STARTOS_&lt;random&gt;</code> (encrypted) or <code>STARTOS_&lt;random&gt;_UNENC</code></li>
<li>LUKS via <code>cryptsetup luksFormat/luksOpen</code> with password-based key</li>
<li>Default password: <code>"password"</code> (changed during setup)</li>
</ul>
<h4>Filesystem: btrfs</h4>
<ul>
<li>Copy-on-Write (COW) snapshots for safe service installs</li>
<li><code>cp --reflink=always</code> for instant volume snapshots before upgrades</li>
<li>If install fails, volumes restored from snapshot automatically</li>
</ul>
<h4>Volume Layout (LVM)</h4>
<ul>
<li><code>main</code> (8 GB) &mdash; System data, Patch-DB</li>
<li><code>package-data</code> (100% remaining) &mdash; All service/app data</li>
</ul>
</div>
</div>
<div class="layer l-os" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Operating System &mdash; Debian Bookworm</span>
<span class="layer-tag badge-amber">OS</span>
</div>
<div class="layer-desc">Debian 12 (same base as Archipelago), systemd services, x86_64 + ARM64 + RISC-V targets</div>
<div class="layer-layman">Same stable Debian foundation as Archipelago. Supports more CPU architectures including RISC-V.</div>
<div class="layer-details">
<h4>Platform Targets</h4>
<ul>
<li><b>x86_64:</b> Standard PCs and servers</li>
<li><b>aarch64:</b> ARM64 (Raspberry Pi 4/5, etc.)</li>
<li><b>riscv64:</b> RISC-V (emerging architecture)</li>
</ul>
</div>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- STARTOS: CONTAINERS -->
<!-- ============================================================ -->
<div class="section" id="sec-start9-containers">
<h3 class="subsection-title">LXC Container Model</h3>
<div class="sec-grid">
<div class="sec-card">
<div class="sec-title">Two-Layer Architecture</div>
<div class="sec-desc"><b>Outer:</b> One LXC container per service, created by the Rust backend. Base rootfs is a read-only SquashFS image mounted as OverlayFS. <b>Inner:</b> Node.js container runtime inside each LXC can launch subcontainers from S9PK-bundled images.</div>
<div class="sec-layman">Each app gets its own sealed Linux environment with a read-only base. Any changes go to a separate overlay layer.</div>
</div>
<div class="sec-card">
<div class="sec-title">Communication</div>
<div class="sec-desc">JSON-RPC over Unix domain sockets. <code>/media/startos/rpc/service.sock</code> (inbound) and <code>host.sock</code> (host callbacks). Services export <code>init()</code>, <code>uninit()</code>, <code>main()</code> via JavaScript ABI.</div>
<div class="sec-layman">Apps talk to the system through socket files, not network ports. Each app implements 3 required JavaScript functions.</div>
</div>
<div class="sec-card">
<div class="sec-title">Isolation</div>
<div class="sec-desc">User namespaces (container UID 0 &rarr; host UID 100000, range 65536). AppArmor profiles with nesting. veth bridge on <code>lxcbr0</code> (10.0.3.x subnet). GPU passthrough support via manifest flag.</div>
<div class="sec-layman">Container root maps to an unprivileged host user. Each container gets its own virtual network interface.</div>
</div>
<div class="sec-card">
<div class="sec-title">Lifecycle</div>
<div class="sec-desc"><code>LxcManager</code> handles creation (30s timeout), garbage collection, and cleanup. Service actors manage per-service state machines. btrfs reflink snapshots before install/upgrade for atomic rollback.</div>
</div>
</div>
<h3 class="subsection-title">S9PK Package Format (v2)</h3>
<div class="sec-grid">
<div class="sec-card">
<div class="sec-title">Signed Merkle Archive</div>
<div class="sec-desc">Ed25519 signatures with prehashed content (SHA-512 over blake3 merkle root). Magic bytes: <code>0x3b 0x3b 0x02</code>. Enables partial downloads, integrity verification of subsets, and efficient delta updates.</div>
<div class="sec-layman">App packages are cryptographically signed and structured so you can verify integrity without downloading the entire thing.</div>
</div>
<div class="sec-card">
<div class="sec-title">Archive Contents</div>
<div class="sec-desc"><code>manifest.json</code> (metadata) + <code>javascript.squashfs</code> (service logic, Node.js) + <code>images/&lt;arch&gt;/*.squashfs</code> (container filesystems per CPU architecture) + <code>assets.squashfs</code> (optional static assets) + <code>icon</code> + <code>LICENSE.md</code></div>
<div class="sec-layman">Each package bundles its own containers, logic code, icon, and license in one downloadable file.</div>
</div>
<div class="sec-card">
<div class="sec-title">Service ABI (JavaScript/Node.js)</div>
<div class="sec-desc">Services implement <code>init()</code>, <code>uninit()</code>, and <code>main()</code> in JavaScript. The container runtime provides an <code>Effects</code> interface for host callbacks (dependency queries, config, health reporting).</div>
<div class="sec-layman">App developers write their service logic in JavaScript. The system provides a standard API for the app to interact with the host.</div>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- STARTOS: PROTOCOLS -->
<!-- ============================================================ -->
<div class="section" id="sec-start9-protocols">
<div class="proto-grid">
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">JSON-RPC (Host &harr; Service)</div>
<div class="proto-card-desc">All communication between the Rust backend and services uses JSON-RPC over Unix domain sockets.</div>
<div class="proto-card-layman">Apps and the system talk through a structured messaging format over local socket files &mdash; fast and secure.</div>
<div class="proto-details">
<b>Transport:</b> Unix domain sockets<br>
<b>Inbound:</b> <code>/media/startos/rpc/service.sock</code><br>
<b>Host callbacks:</b> <code>/media/startos/rpc/host.sock</code> (Effects interface)<br>
<b>Library:</b> <code>rpc-toolkit</code> (custom Rust crate)<br>
<b>Used by:</b> All service containers &harr; startd
</div>
</div>
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">JSON-RPC (UI &harr; Backend)</div>
<div class="proto-card-desc">The Angular frontend communicates with startd via JSON-RPC over HTTP. 100+ API methods. State sync via Patch-DB WebSocket.</div>
<div class="proto-card-layman">The dashboard sends commands and gets responses in JSON format. Live updates stream automatically through a WebSocket.</div>
<div class="proto-details">
<b>Transport:</b> HTTP POST (commands) + WebSocket (state sync)<br>
<b>State sync:</b> Patch-DB pushes CBOR-encoded diffs over WebSocket<br>
<b>Frontend applies:</b> <code>PatchDB.watch$()</code> &rarr; Angular signals via <code>toSignal()</code><br>
<b>Methods:</b> 100+ via <code>ApiService</code> abstract class
</div>
</div>
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">Patch-DB (CBOR Reactive Sync)</div>
<div class="proto-card-desc">Custom reactive database using CBOR encoding. Backend pushes diffs over WebSocket &mdash; UI updates automatically without polling.</div>
<div class="proto-card-layman">Instead of the UI constantly asking "what changed?", the backend pushes only what changed, in a compact binary format.</div>
<div class="proto-details">
<b>Encoding:</b> CBOR (Concise Binary Object Representation, RFC 8949)<br>
<b>Sync model:</b> Server-push diffs, not request-response<br>
<b>Storage:</b> <code>/media/startos/data/main/</code><br>
<b>Advantage:</b> Much smaller than JSON, real-time without polling
</div>
</div>
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">HTTPS / TLS (Built-in)</div>
<div class="proto-card-desc">TLS termination handled directly by the Rust backend (tokio-rustls). Self-signed root CA per server + ACME for public domains.</div>
<div class="proto-card-layman">The backend IS the web server &mdash; no nginx or caddy needed. It handles encryption directly.</div>
<div class="proto-details">
<b>TLS library:</b> tokio-rustls 0.26 + OpenSSL (vendored, for cert generation)<br>
<b>Local certs:</b> Self-signed root CA (NIST P-256 keys)<br>
<b>Public certs:</b> ACME client (<code>async-acme</code>) with TLS-ALPN-01 challenge<br>
<b>Routing:</b> SNI-based virtual hosting via VHostController
</div>
</div>
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">WireGuard (VPN Tunnels)</div>
<div class="proto-card-desc">First-class WireGuard support for remote access. Users add WireGuard configs as "gateways." Managed by tunnelbox daemon.</div>
<div class="proto-card-layman">Built-in VPN for accessing your node from anywhere. Add a WireGuard config and get a secure tunnel.</div>
<div class="proto-details">
<b>Implementation:</b> <code>wg-quick</code> + <code>x25519-dalek</code> (Rust)<br>
<b>Daemon:</b> <code>tunnelbox</code> (symlink of startbox binary)<br>
<b>Multi-gateway:</b> Supports multiple interfaces with separate domain configs
</div>
</div>
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">DNS (hickory-server)</div>
<div class="proto-card-desc">Built-in DNS server for service discovery and resolution. Also uses mDNS (avahi) for .local domain access on LAN.</div>
<div class="proto-card-layman">The system runs its own DNS so containers can find each other by name. Your phone finds the node via .local address.</div>
<div class="proto-details">
<b>Library:</b> hickory-server (formerly trust-dns)<br>
<b>mDNS:</b> avahi-resolve-host-name for <code>.local</code> domains<br>
<b>Container network:</b> <code>lxcbr0</code> bridge, host at 10.0.3.1
</div>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- STARTOS: WEB5 -->
<!-- ============================================================ -->
<div class="section" id="sec-start9-web5">
<div class="not-implemented">
<h3>Not Implemented in StartOS</h3>
<p>StartOS does not implement Web5 (DIDs, DWNs, or Verifiable Credentials).<br>Authentication uses password-based sessions and public/private key signatures.</p>
</div>
</div>
<!-- ============================================================ -->
<!-- STARTOS: SECURITY -->
<!-- ============================================================ -->
<div class="section" id="sec-start9-security">
<div class="sec-grid">
<div class="sec-card">
<div class="sec-title">LXC + User Namespaces</div>
<div class="sec-desc">Each service in its own LXC container with UID/GID mapping (container 0 &rarr; host 100000, range 65536). AppArmor profiles with nesting. OverlayFS rootfs (base read-only).</div>
<div class="sec-layman">Apps run in isolated Linux environments with their own user systems. Container root is mapped to an unprivileged host user.</div>
</div>
<div class="sec-card">
<div class="sec-title">Package Signing (Ed25519)</div>
<div class="sec-desc">All S9PK packages signed with Ed25519 over blake3 merkle roots. Signature verified before installation. Prevents supply chain attacks.</div>
<div class="sec-layman">Every app package is cryptographically signed. If someone tampers with it, the signature check fails and installation is blocked.</div>
</div>
<div class="sec-card">
<div class="sec-title">btrfs Snapshots</div>
<div class="sec-desc">COW filesystem snapshots before every install/upgrade. If an install fails, data is atomically restored to the pre-install state.</div>
<div class="sec-layman">The system takes a snapshot before every app update. If the update fails, your data is automatically rolled back.</div>
</div>
<div class="sec-card">
<div class="sec-title">Authentication</div>
<div class="sec-desc">Password-based + session cookies. Local authcookie for CLI. Public/private key signatures for remote admin. Encrypted wire protocol during setup (public key exchange + encrypted password).</div>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- STARTOS: BOOT SEQUENCE -->
<!-- ============================================================ -->
<div class="section" id="sec-start9-boot">
<div style="margin-bottom: 20px;">
<div class="boot-step">
<div class="boot-num" style="background:rgba(239,68,68,0.2); color:#fca5a5;">1</div>
<div class="boot-content">
<div class="boot-title">Preinit Script</div>
<div class="boot-desc">Optional <code>/media/startos/config/preinit.sh</code> runs before anything else. Enables local auth cookie.</div>
</div>
</div>
<div class="boot-step">
<div class="boot-num" style="background:rgba(168,85,247,0.2); color:#d8b4fe;">2</div>
<div class="boot-content">
<div class="boot-title">Load Database + SSH Keys</div>
<div class="boot-desc">Patch-DB loaded from disk (CBOR format). SSH developer keys written. MOK enrollment for Secure Boot if applicable.</div>
</div>
</div>
<div class="boot-step">
<div class="boot-num" style="background:rgba(6,182,212,0.2); color:#67e8f9;">3</div>
<div class="boot-content">
<div class="boot-title">Network Controller</div>
<div class="boot-desc">DNS server (hickory-server), SOCKS5 proxy, VHost reverse proxy, port forwarding, ACME client, WiFi configuration &mdash; all start together.</div>
</div>
</div>
<div class="boot-step">
<div class="boot-num" style="background:rgba(16,185,129,0.2); color:#6ee7b7;">4</div>
<div class="boot-content">
<div class="boot-title">System Initialization</div>
<div class="boot-desc">Mount logs to data drive, load CA certificate, set CPU governor to performance, NTP clock sync, enable zram, hardware inventory via <code>lshw</code>.</div>
</div>
</div>
<div class="boot-step">
<div class="boot-num" style="background:rgba(99,102,241,0.2); color:#a5b4fc;">5</div>
<div class="boot-content">
<div class="boot-title">Launch Service Intranet + Services</div>
<div class="boot-desc">LXC bridge network (<code>lxcbr0</code>) created. Database validated. Service actors start all installed services. Postinit script runs.</div>
</div>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- STARTOS: DATA PATHS -->
<!-- ============================================================ -->
<div class="section" id="sec-start9-data">
<div class="path-tree"><span class="dir">/media/startos/data/</span> <span class="desc">&larr; Root data directory (optionally LUKS encrypted)</span>
<span class="dir"> ├── main/</span> <span class="desc">System data, Patch-DB (8 GB LVM volume)</span>
<span class="dir"> └── package-data/</span> <span class="desc">All service data (remaining disk space)</span>
<span class="dir"> ├── volumes/{pkg-id}/data/{vol}/</span> <span class="desc">Per-service volume data</span>
<span class="dir"> ├── volumes/{pkg-id}/assets/{ver}/</span> <span class="desc">Per-service read-only assets</span>
<span class="dir"> └── logs/{pkg-id}/</span> <span class="desc">Per-service log output</span>
<span class="dir">/usr/lib/startos/</span> <span class="desc">&larr; System binaries and base images</span>
<span class="dir"> ├── container-runtime/rootfs.squashfs</span> <span class="desc">Base LXC container image</span>
<span class="dir"> └── package/</span> <span class="desc">Mounted JS from S9PK inside containers</span>
<span class="dir">/var/lib/lxc/</span> <span class="desc">LXC container storage</span>
<span class="dir">/media/startos/config/</span> <span class="desc">System config (preinit.sh, postinit.sh, standby)</span>
<span class="dir">/media/startos/backups/</span> <span class="desc">Backup mount points per service</span></div>
</div>
<!-- ============================================================ -->
<!-- STARTOS: GLOSSARY (shared) handled by sec-glossary -->
<!-- ============================================================ -->
<!-- The vs. Archipelago table is appended to the StartOS overview section -->
<!-- Inject it via JavaScript after page load, or just reference sec-start9-overview -->
<!-- (StartOS comparison table moved into sec-start9-overview below) -->
<div style="display:none;" id="sec-start9-compare-data">
<h3 class="subsection-title">vs. Archipelago</h3>
<table class="cmp-table">
<tr><th>Aspect</th><th><span class="sys-tag sys-start9">StartOS</span></th><th><span class="sys-tag sys-archy">Archipelago</span></th></tr>
<tr><td class="cmp-label">Container runtime</td><td>LXC (system containers, AppArmor)</td><td>Podman (rootless OCI containers)</td></tr>
<tr><td class="cmp-label">Reverse proxy</td><td>Built into Rust backend (Axum/Hyper)</td><td>Nginx (external)</td></tr>
<tr><td class="cmp-label">Frontend</td><td>Angular 21 + Taiga UI 5</td><td>Vue 3 + Vite 7 + Tailwind</td></tr>
<tr><td class="cmp-label">API</td><td>JSON-RPC (rpc-toolkit)</td><td>JSON-RPC</td></tr>
<tr><td class="cmp-label">State sync</td><td>Patch-DB (CBOR diffs over WebSocket)</td><td>Pinia stores + JSON Patch over WebSocket</td></tr>
<tr><td class="cmp-label">App packaging</td><td>S9PK v2 (signed merkle archive with JS ABI)</td><td>Container images from private registry</td></tr>
<tr><td class="cmp-label">Disk encryption</td><td>Optional LUKS on LVM</td><td>Mandatory LUKS2 on raw partition</td></tr>
<tr><td class="cmp-label">Filesystem</td><td>btrfs (COW snapshots for safe installs)</td><td>ext4</td></tr>
<tr><td class="cmp-label">Service code</td><td>JavaScript/TypeScript (Node.js SDK)</td><td>Native container entrypoints</td></tr>
<tr><td class="cmp-label">DNS</td><td>Built-in (hickory-server)</td><td>System DNS + container DNS (NetAvark)</td></tr>
<tr><td class="cmp-label">Tor</td><td>Removed in v0.4 (was present in 0.3.x)</td><td>System Tor daemon + hidden services</td></tr>
<tr><td class="cmp-label">VPN</td><td>WireGuard (first-class, tunnelbox)</td><td>Tailscale (WireGuard-based mesh)</td></tr>
<tr><td class="cmp-label">Base OS</td><td>Debian Bookworm</td><td>Debian 12 Bookworm</td></tr>
<tr><td class="cmp-label">Backend language</td><td>Rust (same)</td><td>Rust</td></tr>
<tr><td class="cmp-label">Kiosk display</td><td>No</td><td>X11 + Chromium on VT7</td></tr>
<tr><td class="cmp-label">Identity</td><td>No DID/Web5 support</td><td>did:key + did:dht + VCs + DWN</td></tr>
</table>
</div>
<!-- ============================================================ -->
<!-- UMBRELOS: OVERVIEW -->
<!-- ============================================================ -->
<div class="section" id="sec-umbrelos-overview">
<p style="font-size: 13px; color: #8b8fa3; margin-bottom: 16px;">Architecture analysis sourced from <code style="background:rgba(255,255,255,0.06); padding:1px 6px; border-radius:3px; font-family:'SF Mono',monospace; font-size:11px;">getumbrel/umbrel</code> on GitHub (master branch). Click any layer to expand.</p>
<div class="stats">
<div class="stat"><div class="stat-value">Docker</div><div class="stat-label">Container Runtime</div></div>
<div class="stat"><div class="stat-value">Node.js</div><div class="stat-label">Backend (umbreld)</div></div>
<div class="stat"><div class="stat-value">React 19</div><div class="stat-label">Frontend</div></div>
<div class="stat"><div class="stat-value">Compose</div><div class="stat-label">App Format</div></div>
<div class="stat"><div class="stat-value">None</div><div class="stat-label">Disk Encryption</div></div>
<div class="stat"><div class="stat-value">A/B Boot</div><div class="stat-label">Rugix Partitions</div></div>
</div>
<div class="layer-stack">
<div class="layer l-ui"><div class="layer-header"><span class="layer-name">React 19 + Tailwind 4 + Radix UI</span><span class="layer-tag badge-purple">UI</span></div><div class="layer-desc">Static SPA served by umbreld's Express server. Zustand + TanStack React Query for state. tRPC for typed API. 8+ languages.</div></div>
<div class="layer l-svc"><div class="layer-header"><span class="layer-name">umbreld (Node.js 22 / TypeScript)</span><span class="layer-tag badge-blue">Service</span></div><div class="layer-desc">Single daemon on port 80: Express + tRPC API, app lifecycle via Docker Compose, file manager, backups (Kopia), terminal (node-pty).</div></div>
<div class="layer l-cnt"><div class="layer-header"><span class="layer-name">Docker 28.5 (rootful)</span><span class="layer-tag badge-green">Containers</span></div><div class="layer-desc">Each app is a Docker Compose project. Flat bridge network (10.21.0.0/16). Per-app auth proxy containers. All containers destroyed on boot.</div></div>
<div class="layer l-net"><div class="layer-header"><span class="layer-name">Networking</span><span class="layer-tag badge-cyan">Network</span></div><div class="layer-desc">No reverse proxy. Express serves on :80 directly. Optional Tor (containerized). mDNS via avahi. No TLS by default.</div></div>
<div class="layer l-os"><div class="layer-header"><span class="layer-name">Debian Trixie (testing) + Rugix</span><span class="layer-tag badge-amber">OS</span></div><div class="layer-desc">Date-pinned Debian testing. Rugix A/B root partitions for atomic OS updates with automatic rollback. /data partition persists.</div></div>
</div>
</div>
<div class="section" id="sec-umbrelos-layers">
<div class="layer-stack">
<div class="layer l-ui" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Web UI &mdash; React 19</span>
<span class="layer-tag badge-purple">Application</span>
</div>
<div class="layer-desc">React 19 + TypeScript + Vite 6 + Tailwind 4 + Radix UI, served as static SPA by umbreld's Express server</div>
<div class="layer-layman">The dashboard you use in your browser. Built with React (Meta's web framework), styled with Tailwind.</div>
<div class="layer-details">
<h4>Tech Stack</h4>
<ul>
<li><b>Framework:</b> React 19 + TypeScript (strict)</li>
<li><b>Build:</b> Vite 6</li>
<li><b>Styling:</b> Tailwind CSS 4 + Radix UI primitives + shadcn/ui patterns</li>
<li><b>State:</b> Zustand (client) + TanStack React Query v5 (server)</li>
<li><b>API:</b> tRPC React Query v11 for typed RPC</li>
<li><b>i18n:</b> i18next (8+ languages)</li>
<li><b>Animations:</b> Framer Motion (as <code>motion</code> package)</li>
<li><b>Terminal:</b> xterm.js for in-browser terminal</li>
<li><b>Charts:</b> Recharts for data visualization</li>
</ul>
</div>
</div>
<div class="layer l-svc" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Backend &mdash; umbreld (Node.js/TypeScript)</span>
<span class="layer-tag badge-blue">Service</span>
</div>
<div class="layer-desc">Single Node.js 22 daemon handling web server, tRPC API, app lifecycle, Tor, backups, file management, and OS updates</div>
<div class="layer-layman">The brain of the system. A TypeScript process that does everything &mdash; web server, app manager, backup handler.</div>
<div class="layer-details">
<h4>Key Modules</h4>
<ul>
<li><b>Server:</b> Express 4 + tRPC v11 over HTTP and WebSocket on port 80</li>
<li><b>Apps:</b> Docker Compose lifecycle (install, start, stop, update, uninstall)</li>
<li><b>AppStore:</b> Git-based &mdash; clones <code>getumbrel/umbrel-apps</code>, pulls every 5 minutes</li>
<li><b>User:</b> Single-user JWT auth (bcrypt $2b$, 12 rounds) + optional TOTP 2FA</li>
<li><b>Files:</b> File browser with Samba sharing, thumbnails, external storage</li>
<li><b>Hardware:</b> RAID (ZFS) for Umbrel Home Pro, internal/external storage detection</li>
<li><b>Backups:</b> Kopia v0.19.0 encrypted backups to external drives</li>
<li><b>Notifications:</b> In-app notification system + widgets</li>
<li><b>Terminal:</b> WebSocket-based terminal (node-pty + xterm.js)</li>
<li><b>Dbus:</b> D-Bus interface to systemd for reboot/shutdown/hostname</li>
</ul>
<h4>State Storage</h4>
<ul>
<li><b>YAML file:</b> <code>umbrel.yaml</code> &mdash; no database, just a YAML file</li>
<li>Validation: Zod schemas</li>
<li>Docker: <code>dockerode</code> library + <code>execa</code> shell calls</li>
<li>Git: <code>isomorphic-git</code> for app store management</li>
</ul>
<h4>Legacy Compat Layer</h4>
<ul>
<li>App lifecycle handled by a large <b>bash script</b> (<code>app-script</code>)</li>
<li>Shells out to: docker compose, yq, envsubst, openssl</li>
<li>Explicitly labeled "legacy" in the codebase</li>
</ul>
<h4>Systemd</h4>
<ul>
<li><code>umbrel.service</code>: After=network-online.target docker.service</li>
<li>Restart=always, 15-minute stop timeout, StartLimitInterval=0</li>
</ul>
</div>
</div>
<div class="layer l-cnt" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Container Layer &mdash; Docker (rootful)</span>
<span class="layer-tag badge-green">Isolation</span>
</div>
<div class="layer-desc">Docker 28.5.0 (rootful, not rootless) + Docker Compose v2. Each app is a separate Compose project.</div>
<div class="layer-layman">Apps run in Docker containers managed by Docker Compose. Unlike Podman, Docker runs as root &mdash; simpler but less isolated.</div>
<div class="layer-details">
<h4>Docker Setup</h4>
<ul>
<li>Installed via official Docker install script, pinned to v28.5.0</li>
<li>Rootful (runs as root) &mdash; not rootless</li>
<li>Each app: separate Docker Compose project (<code>--project-name &lt;app-id&gt;</code>)</li>
<li>Legacy container naming: <code>&lt;app-id&gt;_&lt;service&gt;_1</code> for DNS compat</li>
</ul>
<h4>Network: Flat Bridge</h4>
<ul>
<li>Single shared network: <code>umbrel_main_network</code> (10.21.0.0/16)</li>
<li><b>All apps share one flat network</b> &mdash; any container can talk to any other</li>
<li>Static IPs assigned per service (defined in <code>exports.sh</code>)</li>
<li>No per-app network isolation</li>
</ul>
<h4>Per-App Proxy</h4>
<ul>
<li>Each app gets an <code>app_proxy</code> container (Node.js Express, <code>getumbrel/app-proxy</code>)</li>
<li>Handles JWT authentication for iframe embedding</li>
<li>Proxies to the actual app container on its internal port</li>
<li>UI renders apps in iframes pointing to proxy port</li>
</ul>
<h4>Boot Cleanup</h4>
<ul>
<li>On every startup: stops ALL containers, prunes ALL networks</li>
<li>Prevents stale state from previous versions</li>
<li>Pre-loads images from <code>/images/</code> (tor, auth-server baked into ISO)</li>
</ul>
</div>
</div>
<div class="layer l-net" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Network Layer</span>
<span class="layer-tag badge-cyan">Network</span>
</div>
<div class="layer-desc">No traditional reverse proxy. umbreld serves on port 80. Per-app proxy containers. Optional Tor via container.</div>
<div class="layer-layman">No nginx, no caddy &mdash; the backend itself serves on port 80. Each app has its own mini proxy container for authentication.</div>
<div class="layer-details">
<h4>HTTP</h4>
<ul>
<li>umbreld's Express server listens directly on port 80</li>
<li>Serves UI static files + tRPC API</li>
<li>No port 443/TLS by default &mdash; HTTP only on LAN</li>
<li>mDNS via avahi for <code>HOSTNAME.local</code> access</li>
</ul>
<h4>Tor (Optional)</h4>
<ul>
<li>Toggle per-system (not per-app)</li>
<li><code>tor_proxy</code> container on 10.21.21.11 (SOCKS5)</li>
<li>Each app gets a <code>tor_server</code> container creating hidden services</li>
<li>Dashboard also gets its own hidden service</li>
<li>Provides end-to-end encryption for remote access</li>
</ul>
<h4>Inter-App Communication</h4>
<ul>
<li>Via static IPs on the flat 10.21.0.0/16 bridge network</li>
<li>No DNS-based service discovery &mdash; IPs hardcoded in <code>exports.sh</code></li>
</ul>
</div>
</div>
<div class="layer l-os" onclick="this.classList.toggle('expanded')">
<div class="layer-header">
<span class="layer-name">Operating System &mdash; Debian Trixie (testing)</span>
<span class="layer-tag badge-amber">OS</span>
</div>
<div class="layer-desc">Debian Trixie (testing branch, not stable), built from date-pinned snapshot for reproducibility, Rugix A/B partitions</div>
<div class="layer-layman">Uses Debian's "testing" branch (less stable than Bookworm). Has a clever A/B partition system for safe OS updates.</div>
<div class="layer-details">
<h4>OS Build</h4>
<ul>
<li>Built inside Docker via multi-stage Dockerfile (<code>umbrelos.Dockerfile</code>)</li>
<li>Date-pinned Debian snapshot (e.g., 20251229) for reproducibility</li>
<li>Includes: NetworkManager, avahi, systemd-timesyncd, Bluetooth, SSH</li>
<li>Node.js 22.13.0 baked in</li>
</ul>
<h4>Rugix A/B Partitions</h4>
<ul>
<li>Two root partitions &mdash; active and standby</li>
<li>OS updates write to inactive partition, then swap on reboot</li>
<li>If boot fails, automatic rollback to previous partition</li>
<li>Root filesystem committed after successful boot</li>
</ul>
<h4>Persistent Bind Mounts</h4>
<ul>
<li><code>/var/log</code> &rarr; <code>/data/umbrel-os/var/log</code></li>
<li><code>/var/lib/docker</code> &rarr; <code>/data/umbrel-os/var/lib/docker</code></li>
<li><code>/home</code> &rarr; <code>/data/umbrel-os/home</code></li>
<li>Separate <code>/data</code> partition persists across OS updates</li>
</ul>
<h4>User</h4>
<ul>
<li><code>umbrel</code> (UID 1000), default password <code>umbrel</code></li>
<li>Synced to web UI password after onboarding</li>
<li>Has sudo access</li>
</ul>
</div>
</div>
</div>
</div>
<div class="section" id="sec-umbrelos-containers">
<h3 class="subsection-title">Docker Container Model</h3>
<div class="sec-grid">
<div class="sec-card">
<div class="sec-title">Docker 28.5 (Rootful)</div>
<div class="sec-desc">Docker daemon runs as root (not rootless). Each app is a separate Docker Compose v2 project (<code>--project-name &lt;app-id&gt;</code>). Legacy container naming: <code>&lt;app-id&gt;_&lt;service&gt;_1</code> for DNS compatibility.</div>
<div class="sec-layman">Standard Docker running with root permissions. Each app is managed as a Compose project with its own containers.</div>
</div>
<div class="sec-card">
<div class="sec-title">Flat Network (No Isolation)</div>
<div class="sec-desc">Single shared bridge: <code>umbrel_main_network</code> (10.21.0.0/16). <b>All apps share one network</b> &mdash; any container can communicate with any other. Static IPs assigned per service via <code>exports.sh</code>.</div>
<div class="sec-layman">All apps are on the same network. A compromised app could potentially reach other apps' services directly.</div>
</div>
<div class="sec-card">
<div class="sec-title">Per-App Auth Proxy</div>
<div class="sec-desc">Each app gets an <code>app_proxy</code> container (<code>getumbrel/app-proxy</code>, Node.js Express) that handles JWT authentication for iframe embedding. Proxies to the actual app on its internal port.</div>
<div class="sec-layman">Each app has a mini web server in front of it that checks your login before letting you in.</div>
</div>
<div class="sec-card">
<div class="sec-title">Boot Cleanup</div>
<div class="sec-desc">On every startup: <b>stops ALL containers, prunes ALL networks</b> to prevent stale state. Pre-loads images from <code>/images/</code> (tor, auth-server baked into ISO).</div>
<div class="sec-layman">Every reboot starts fresh by destroying all containers and recreating them. Clean but adds startup time.</div>
</div>
</div>
<h3 class="subsection-title">App Packaging</h3>
<div class="sec-grid">
<div class="sec-card">
<div class="sec-title">umbrel-app.yml</div>
<div class="sec-desc">App manifest with: id, name, version, port, category, dependencies, permissions (GPU), gallery images, release notes, widgets, torOnly flag, installSize. Validated by Zod schema.</div>
<div class="sec-layman">A YAML file describing the app &mdash; what it does, what it needs, what ports it uses, and how to display it in the store.</div>
</div>
<div class="sec-card">
<div class="sec-title">docker-compose.yml</div>
<div class="sec-desc">Standard Docker Compose v3.7. Services reference <code>umbrel_main_network</code>. Images pinned by SHA256 digest. Apps define their own security constraints (no enforced capability dropping).</div>
<div class="sec-layman">Standard Docker Compose file that defines the app's containers, networks, and volumes.</div>
</div>
<div class="sec-card">
<div class="sec-title">exports.sh + hooks/</div>
<div class="sec-desc"><code>exports.sh</code> exports environment variables (IPs, ports, credentials) for dependency resolution. <code>hooks/</code> directory with lifecycle scripts: pre/post-install, pre/post-start, pre/post-stop, pre/post-update, pre-uninstall.</div>
<div class="sec-layman">Shell scripts that set up environment variables so apps can find each other, plus hooks that run at key lifecycle moments.</div>
</div>
<div class="sec-card">
<div class="sec-title">App Store (Git repo)</div>
<div class="sec-desc">Apps distributed via Git repository (<code>getumbrel/umbrel-apps</code>). Cloned locally, pulled every 5 minutes. Community app stores supported. <code>implements</code> field enables alternative implementations (e.g., Bitcoin Knots for Bitcoin Core).</div>
<div class="sec-layman">The app store is just a Git repository. Umbrel checks for updates every 5 minutes by pulling the latest commits.</div>
</div>
</div>
</div>
<div class="section" id="sec-umbrelos-protocols">
<div class="proto-grid">
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">tRPC (UI &harr; Backend)</div>
<div class="proto-card-desc">TypeScript-first RPC framework with end-to-end type safety. Runs over both HTTP and WebSocket on port 80.</div>
<div class="proto-card-layman">A typed communication channel between the dashboard and the backend. If the API changes, TypeScript catches errors automatically.</div>
<div class="proto-details">
<b>Version:</b> tRPC v11<br>
<b>Transport:</b> HTTP + WebSocket (via Express 4)<br>
<b>Port:</b> 80 (Express serves both UI and API)<br>
<b>Type safety:</b> Server types flow directly to client (TanStack React Query v5)<br>
<b>Used by:</b> React 19 frontend &harr; umbreld
</div>
</div>
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">Docker Compose (App Lifecycle)</div>
<div class="proto-card-desc">Each app managed via Docker Compose v2. Install/start/stop/update handled by a bash script (app-script) calling docker compose.</div>
<div class="proto-card-layman">Apps are defined as Docker Compose projects. A bash script handles the lifecycle by calling docker compose commands.</div>
<div class="proto-details">
<b>Compose version:</b> v2 (docker compose plugin, not docker-compose binary)<br>
<b>Lifecycle script:</b> <code>app-script</code> (bash, labeled "legacy")<br>
<b>Tools used:</b> docker compose, yq, envsubst, openssl<br>
<b>Hooks:</b> pre/post-install, pre/post-start, pre/post-stop, pre/post-update, pre-uninstall
</div>
</div>
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">exports.sh (Dependency Resolution)</div>
<div class="proto-card-desc">Shell scripts that export environment variables (IPs, ports, RPC credentials). When app B depends on app A, A's exports.sh is sourced first.</div>
<div class="proto-card-layman">Apps share their connection details through environment variables set by shell scripts.</div>
<div class="proto-details">
<b>Variables exported:</b> IP addresses (static), ports, RPC passwords, hidden service hostnames<br>
<b>Resolution:</b> Transitive deps resolved in post-order (depth-first)<br>
<b>Alternative implementations:</b> <code>settings.yml</code> can map dependency (e.g., bitcoin &rarr; bitcoin-knots)<br>
<b>Deps NOT auto-installed:</b> UI warns users to install dependencies first
</div>
</div>
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">Tor (Optional, Containerized)</div>
<div class="proto-card-desc">Toggle per-system. tor_proxy container provides SOCKS5 at 10.21.21.11. Per-app tor_server containers create hidden services.</div>
<div class="proto-card-layman">Tor is optional and runs in its own container. When enabled, each app gets its own .onion address for remote access.</div>
<div class="proto-details">
<b>SOCKS5:</b> <code>10.21.21.11</code> (tor_proxy container)<br>
<b>Per-app:</b> tor_server container creates hidden service pointing to app_proxy<br>
<b>Dashboard:</b> Also gets its own hidden service<br>
<b>Provides:</b> End-to-end encryption for remote access (since no TLS by default)
</div>
</div>
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">JWT + Proxy Tokens (Auth)</div>
<div class="proto-card-desc">JWT for API authentication. Separate "proxy tokens" validate iframe requests to app_proxy containers. bcrypt password hashing.</div>
<div class="proto-card-layman">Login tokens that prove who you are. Separate tokens for the dashboard API and for accessing individual apps.</div>
<div class="proto-details">
<b>API auth:</b> JWT (jsonwebtoken library)<br>
<b>Password:</b> bcrypt ($2b$, 12 rounds)<br>
<b>App auth:</b> UMBREL_PROXY_TOKEN cookie validated by app_proxy containers<br>
<b>2FA:</b> Optional TOTP
</div>
</div>
<div class="proto-card" onclick="toggleProto(this)">
<div class="proto-card-name">Git (App Store)</div>
<div class="proto-card-desc">App store is a Git repository cloned locally via isomorphic-git. Pulled every 5 minutes for updates.</div>
<div class="proto-card-layman">The app catalog is just a Git repo. Umbrel checks for new apps and updates by pulling the latest commits every 5 minutes.</div>
<div class="proto-details">
<b>Default repo:</b> <code>getumbrel/umbrel-apps</code> (GitHub)<br>
<b>Library:</b> isomorphic-git (pure JS Git implementation)<br>
<b>Pull interval:</b> Every 5 minutes<br>
<b>Community stores:</b> Supported (add custom Git URLs)
</div>
</div>
</div>
</div>
<div class="section" id="sec-umbrelos-web5">
<div class="not-implemented">
<h3>Not Implemented in umbrelOS</h3>
<p>umbrelOS does not implement Web5 (DIDs, DWNs, or Verifiable Credentials).<br>Authentication uses a single-user JWT model. Per-app passwords are derived from a deterministic seed via HMAC-SHA256.</p>
</div>
</div>
<div class="section" id="sec-umbrelos-security">
<div class="sec-grid">
<div class="sec-card">
<div class="sec-title" style="color: #fca5a5;">No Disk Encryption</div>
<div class="sec-desc">No LUKS, no dm-crypt. All data stored unencrypted on disk. Backups use Kopia with per-repository passwords. A deterministic seed (256-byte random token) derives per-app passwords via HMAC-SHA256.</div>
<div class="sec-layman">If someone physically steals the drive, all data is readable. No encryption at rest.</div>
</div>
<div class="sec-card">
<div class="sec-title" style="color: #fca5a5;">Flat Network (No App Isolation)</div>
<div class="sec-desc">All apps share one Docker bridge (10.21.0.0/16). Any container can communicate with any other container. The app_proxy adds authentication but not network isolation.</div>
<div class="sec-layman">All apps are on the same network. A compromised app could potentially access other apps' services.</div>
</div>
<div class="sec-card">
<div class="sec-title">Authentication</div>
<div class="sec-desc">Single user model. Password hashed with bcrypt ($2b$, 12 rounds). JWT tokens for API auth. Separate "proxy tokens" for app iframe auth. Optional TOTP 2FA. Session cookie: <code>UMBREL_PROXY_TOKEN</code>.</div>
</div>
<div class="sec-card">
<div class="sec-title">Rootful Docker</div>
<div class="sec-desc">Docker daemon runs as root. Containers run as UID 1000 where possible, but no enforced capability dropping or security profiles. No <code>--cap-drop=ALL</code>, no <code>no-new-privileges</code> by default.</div>
<div class="sec-layman">Docker has root access to the machine. Individual containers may or may not restrict their own privileges.</div>
</div>
</div>
</div>
<div class="section" id="sec-umbrelos-boot">
<div style="margin-bottom: 20px;">
<div class="boot-step">
<div class="boot-num" style="background:rgba(239,68,68,0.2); color:#fca5a5;">1</div>
<div class="boot-content">
<div class="boot-title">GRUB / Rugix &rarr; Select Active Partition</div>
<div class="boot-desc">GRUB (amd64) or tryboot (RPi) loads the kernel from the active A/B partition. Rugix commits to current partition on successful boot.</div>
</div>
</div>
<div class="boot-step">
<div class="boot-num" style="background:rgba(245,158,11,0.2); color:#fcd34d;">2</div>
<div class="boot-content">
<div class="boot-title">systemd &rarr; Docker &rarr; umbreld</div>
<div class="boot-desc">systemd starts, brings up networking (NetworkManager) and Docker daemon. <code>umbrel.service</code> starts <code>umbreld --data-directory=/home/umbrel/umbrel</code>.</div>
</div>
</div>
<div class="boot-step">
<div class="boot-num" style="background:rgba(139,92,246,0.2); color:#c4b5fd;">3</div>
<div class="boot-content">
<div class="boot-title">umbreld Initialization</div>
<div class="boot-desc">Runs startup migrations, syncs system password, restores WiFi, waits for NTP sync (10s, important for RPi with no RTC).</div>
</div>
</div>
<div class="boot-step">
<div class="boot-num" style="background:rgba(239,68,68,0.2); color:#fca5a5;">4</div>
<div class="boot-content">
<div class="boot-title">Docker Clean Slate</div>
<div class="boot-desc"><b>Stops and removes ALL containers, prunes ALL networks.</b> Pre-loads images from <code>/images/</code>. Prevents stale state from previous versions.</div>
<div class="boot-detail">Destructive reset on every boot &mdash; ensures clean state but adds startup time</div>
</div>
</div>
<div class="boot-step">
<div class="boot-num" style="background:rgba(16,185,129,0.2); color:#6ee7b7;">5</div>
<div class="boot-content">
<div class="boot-title">Start App Environment + All Apps</div>
<div class="boot-desc">Starts <code>tor_proxy</code> + <code>auth</code> containers first, then all installed apps in parallel. Express HTTP server starts on port 80. App store update loop begins (every 5 min).</div>
</div>
</div>
</div>
</div>
<div class="section" id="sec-umbrelos-data">
<div class="path-tree"><span class="dir">/home/umbrel/umbrel/</span> <span class="desc">&larr; Main data directory (NO encryption)</span>
<span class="dir"> ├── umbrel.yaml</span> <span class="desc">Main config/state (YAML file, not a database)</span>
<span class="dir"> ├── app-data/{app-id}/</span> <span class="desc">Per-app data, compose files, manifests</span>
<span class="dir"> ├── app-stores/</span> <span class="desc">Git clones of app store repositories</span>
<span class="dir"> ├── tor/data/app-{id}/hostname</span> <span class="desc">Per-app .onion addresses</span>
<span class="dir"> ├── db/umbrel-seed/seed</span> <span class="desc">Deterministic seed (256-byte) for per-app passwords</span>
<span class="dir"> ├── secrets/jwt</span> <span class="desc">JWT signing secret</span>
<span class="dir"> └── home/</span> <span class="desc">User files, backups</span>
<span class="dir">/opt/umbreld/</span> <span class="desc">umbreld daemon (npm-linked)</span>
<span class="dir">/opt/umbreld/ui/</span> <span class="desc">React SPA static files</span>
<span class="dir">/images/</span> <span class="desc">Pre-loaded Docker images (tor, auth-server)</span></div>
</div>
<!-- umbrelOS comparison stored for display in overview -->
<div style="display:none;" id="sec-umbrelos-compare-data">
<table class="cmp-table">
<tr><th>Aspect</th><th><span class="sys-tag sys-umbrel">umbrelOS</span></th><th><span class="sys-tag sys-archy">Archipelago</span></th></tr>
<tr><td class="cmp-label">Container runtime</td><td>Docker 28.5 (rootful)</td><td>Podman (rootless)</td></tr>
<tr><td class="cmp-label">Backend language</td><td>TypeScript / Node.js 22</td><td>Rust</td></tr>
<tr><td class="cmp-label">Frontend</td><td>React 19 + Vite 6 + Tailwind 4</td><td>Vue 3 + Vite 7 + Tailwind</td></tr>
<tr><td class="cmp-label">API</td><td>tRPC v11 (typed RPC over HTTP/WS)</td><td>JSON-RPC 2.0</td></tr>
<tr><td class="cmp-label">Reverse proxy</td><td>None (Express on :80 + per-app proxy containers)</td><td>Nginx (ports 80/443, TLS, rate limiting, CSP)</td></tr>
<tr><td class="cmp-label">App packaging</td><td>docker-compose.yml + umbrel-app.yml + exports.sh</td><td>Backend manifests + container images from registry</td></tr>
<tr><td class="cmp-label">App store</td><td>Git repository (pulled every 5 min)</td><td>Built-in marketplace with curated apps</td></tr>
<tr><td class="cmp-label">State storage</td><td>YAML file (<code>umbrel.yaml</code>)</td><td>Rust backend in-memory + file persistence</td></tr>
<tr><td class="cmp-label">Disk encryption</td><td><b style="color:#fca5a5;">None</b></td><td>Mandatory LUKS2 (AES-XTS or ChaCha20-Adiantum)</td></tr>
<tr><td class="cmp-label">Network isolation</td><td><b style="color:#fca5a5;">Flat bridge (10.21.0.0/16, all apps share)</b></td><td>Per-app isolation (archy-net + bridge + host)</td></tr>
<tr><td class="cmp-label">Capability dropping</td><td>Not enforced by default</td><td><code>--cap-drop=ALL</code> + only needed caps</td></tr>
<tr><td class="cmp-label">OS updates</td><td>Rugix A/B partitions (atomic, rollback)</td><td>ISO reflash / manual upgrade</td></tr>
<tr><td class="cmp-label">Base OS</td><td>Debian Trixie (testing)</td><td>Debian 12 Bookworm (stable)</td></tr>
<tr><td class="cmp-label">Tor</td><td>Optional (containerized)</td><td>System daemon + hidden services</td></tr>
<tr><td class="cmp-label">TLS</td><td>None (HTTP only on LAN)</td><td>Self-signed cert on :443 + HSTS</td></tr>
<tr><td class="cmp-label">Boot behavior</td><td>Destroys all containers on every boot</td><td>Crash recovery + container state snapshots</td></tr>
<tr><td class="cmp-label">Identity</td><td>Deterministic seed for app passwords only</td><td>did:key + did:dht + VCs + DWN + Nostr</td></tr>
<tr><td class="cmp-label">Kiosk display</td><td>No</td><td>X11 + Chromium on VT7</td></tr>
<tr><td class="cmp-label">Security headers</td><td>None (no proxy)</td><td>CSP, HSTS, X-Frame-Options, Permissions-Policy</td></tr>
<tr><td class="cmp-label">Rate limiting</td><td>None</td><td>Nginx rate limits (auth 3/s, RPC 20/s, P2P 10/s)</td></tr>
</table>
</div>
<!-- ============================================================ -->
<!-- COMPARISON VIEW (three-column side-by-side) -->
<!-- ============================================================ -->
<div class="section" id="sec-compare-overview">
<div class="stats">
<div class="stat"><div class="stat-value">3</div><div class="stat-label">Systems Compared</div></div>
<div class="stat"><div class="stat-value">Rust</div><div class="stat-label">2/3 Backends</div></div>
<div class="stat"><div class="stat-value">Debian</div><div class="stat-label">3/3 Base OS</div></div>
<div class="stat"><div class="stat-value">3</div><div class="stat-label">Container Runtimes</div></div>
<div class="stat"><div class="stat-value">3</div><div class="stat-label">Frontend Frameworks</div></div>
</div>
<div style="overflow-x: auto;">
<table class="cmp-table" style="min-width: 900px;">
<tr>
<th style="min-width:140px;">Aspect</th>
<th><span class="sys-tag sys-archy">Archipelago</span></th>
<th><span class="sys-tag sys-start9">StartOS</span></th>
<th><span class="sys-tag sys-umbrel">umbrelOS</span></th>
<th style="color:#fcd34d;">Notes / Trade-offs</th>
</tr>
<tr><td colspan="5" class="compare-row-label">Core Architecture</td></tr>
<tr>
<td class="cmp-label">Backend Language</td>
<td class="good">Rust</td>
<td class="good">Rust</td>
<td>TypeScript / Node.js 22</td>
<td class="notes">Rust: memory safety, performance, no GC pauses. Node.js: faster prototyping, larger ecosystem, but runtime overhead.</td>
</tr>
<tr>
<td class="cmp-label">Frontend</td>
<td>Vue 3 + Vite 7 + Tailwind</td>
<td>Angular 21 + Taiga UI 5</td>
<td>React 19 + Vite 6 + Tailwind 4</td>
<td class="notes">All modern choices. Angular is heaviest (TypeScript-only). Vue/React are lighter. Tailwind enables rapid UI iteration.</td>
</tr>
<tr>
<td class="cmp-label">API Protocol</td>
<td>JSON-RPC 2.0</td>
<td>JSON-RPC (rpc-toolkit)</td>
<td>tRPC v11</td>
<td class="notes">JSON-RPC is standard and language-agnostic. tRPC gives end-to-end TypeScript type safety but couples frontend/backend.</td>
</tr>
<tr>
<td class="cmp-label">State Sync</td>
<td>Pinia + JSON Patch over WebSocket</td>
<td>Patch-DB (CBOR diffs over WebSocket)</td>
<td>Zustand + TanStack React Query</td>
<td class="notes">Patch-DB is most efficient (binary diffs). Archipelago and StartOS push updates; Umbrel polls via React Query.</td>
</tr>
<tr>
<td class="cmp-label">Reverse Proxy</td>
<td class="good">Nginx (external, battle-tested)</td>
<td>Built into Rust backend (Axum/Hyper)</td>
<td class="bad">None (Express on :80 + per-app proxy containers)</td>
<td class="notes">Nginx: proven, configurable, rate limiting. Built-in: fewer moving parts. Umbrel: no central proxy means no rate limiting or security headers.</td>
</tr>
<tr><td colspan="5" class="compare-row-label">Container Isolation</td></tr>
<tr>
<td class="cmp-label">Container Runtime</td>
<td class="good">Podman (rootless, OCI)</td>
<td>LXC (system containers, AppArmor)</td>
<td class="warn">Docker 28.5 (rootful)</td>
<td class="notes">Podman: no daemon, rootless by design. LXC: heavier isolation (full system containers). Docker: rootful daemon is a larger attack surface.</td>
</tr>
<tr>
<td class="cmp-label">Rootless Containers</td>
<td class="good">Yes (all containers as UID 1000)</td>
<td class="warn">User namespaces (UID 0 &rarr; host 100000)</td>
<td class="bad">No (Docker daemon runs as root)</td>
<td class="notes">Rootless Podman: container escape = unprivileged user. LXC: namespace mapping mitigates. Docker rootful: escape = root on host.</td>
</tr>
<tr>
<td class="cmp-label">Capability Dropping</td>
<td class="good"><code>--cap-drop=ALL</code> + whitelist</td>
<td class="warn">AppArmor profiles (generated)</td>
<td class="bad">Not enforced by default</td>
<td class="notes">Archipelago: explicit least-privilege. StartOS: AppArmor provides MAC. Umbrel: apps define their own security (inconsistent).</td>
</tr>
<tr>
<td class="cmp-label">Network Isolation</td>
<td class="good">Per-tier networks (archy-net + bridge)</td>
<td class="good">Per-service veth on lxcbr0</td>
<td class="bad">Flat bridge (10.21.0.0/16, all apps share)</td>
<td class="notes">Archipelago/StartOS: apps can't see each other unless connected. Umbrel: any container can reach any other.</td>
</tr>
<tr>
<td class="cmp-label">Memory Limits</td>
<td class="good">Per-container (128MB&ndash;4GB)</td>
<td>Configurable via manifest</td>
<td class="warn">Not enforced by default</td>
<td class="notes">Memory limits prevent a single app from consuming all RAM and crashing the system.</td>
</tr>
<tr><td colspan="5" class="compare-row-label">Security</td></tr>
<tr>
<td class="cmp-label">Disk Encryption</td>
<td class="good">Mandatory LUKS2 (AES-XTS or ChaCha20-Adiantum)</td>
<td class="warn">Optional LUKS on LVM/btrfs</td>
<td class="bad">None</td>
<td class="notes">Physical theft risk: Archipelago data is unreadable. StartOS depends on user choice. Umbrel data is fully exposed.</td>
</tr>
<tr>
<td class="cmp-label">Security Headers</td>
<td class="good">CSP, HSTS, X-Frame-Options, Permissions-Policy</td>
<td class="warn">Partial (built-in proxy)</td>
<td class="bad">None (no central proxy)</td>
<td class="notes">Headers prevent XSS, clickjacking, and protocol downgrade attacks. Critical for browser-based management.</td>
</tr>
<tr>
<td class="cmp-label">Rate Limiting</td>
<td class="good">Auth 3/s, RPC 20/s, P2P 10/s</td>
<td class="warn">RBAC via method metadata</td>
<td class="bad">None</td>
<td class="notes">Rate limiting prevents brute-force password attacks and API abuse.</td>
</tr>
<tr>
<td class="cmp-label">TLS</td>
<td class="good">Self-signed cert on :443 + HSTS</td>
<td class="good">Self-signed CA + ACME (Let's Encrypt)</td>
<td class="bad">None (HTTP only on LAN)</td>
<td class="notes">Without TLS, any device on the LAN can intercept credentials. Tor provides encryption for remote but not LAN access.</td>
</tr>
<tr>
<td class="cmp-label">Auth Model</td>
<td class="good">RBAC (Admin/Viewer/AppUser) + CSRF + session cookies</td>
<td>Password + session cookies + key signatures</td>
<td>Single-user JWT + optional TOTP</td>
<td class="notes">Archipelago supports multiple roles. Others are single-user only.</td>
</tr>
<tr><td colspan="5" class="compare-row-label">App Ecosystem</td></tr>
<tr>
<td class="cmp-label">App Format</td>
<td>Container images from private registry</td>
<td>S9PK v2 (signed merkle archive)</td>
<td>docker-compose.yml + umbrel-app.yml</td>
<td class="notes">S9PK: most sophisticated (signed, partial downloads, delta updates). Compose: simplest for developers. Registry: fast deployment.</td>
</tr>
<tr>
<td class="cmp-label">Package Signing</td>
<td class="warn">Registry-based trust</td>
<td class="good">Ed25519 over blake3 merkle roots</td>
<td class="bad">Docker image digests only</td>
<td class="notes">StartOS has the strongest supply chain security. Archipelago trusts its private registry. Umbrel relies on Docker content trust.</td>
</tr>
<tr>
<td class="cmp-label">App Store</td>
<td>Built-in marketplace (curated)</td>
<td>Registry-based (marketplace)</td>
<td>Git repository (pulled every 5 min)</td>
<td class="notes">Git-based: easy for devs to contribute. Registry: more control. Curated: quality gate but slower additions.</td>
</tr>
<tr>
<td class="cmp-label">Update Mechanism</td>
<td>ISO reflash / manual upgrade</td>
<td>Registry-based OTA</td>
<td class="good">Rugix A/B partitions (atomic, rollback)</td>
<td class="notes">Umbrel has the smoothest update path with automatic rollback. Archipelago's ISO approach is most disruptive.</td>
</tr>
<tr><td colspan="5" class="compare-row-label">Networking & Privacy</td></tr>
<tr>
<td class="cmp-label">Tor</td>
<td class="good">System daemon + hidden services (always available)</td>
<td class="warn">Removed in v0.4 (planned re-integration)</td>
<td>Optional (containerized)</td>
<td class="notes">Archipelago: Tor is first-class. StartOS temporarily lost Tor in 0.4 rewrite. Umbrel: toggle on/off.</td>
</tr>
<tr>
<td class="cmp-label">VPN</td>
<td>Tailscale (WireGuard mesh)</td>
<td>WireGuard (first-class, tunnelbox)</td>
<td class="warn">None built-in</td>
<td class="notes">Both Archipelago and StartOS offer remote access without port forwarding. Umbrel relies on Tor or manual setup.</td>
</tr>
<tr>
<td class="cmp-label">DNS</td>
<td>System DNS + container NetAvark DNS</td>
<td class="good">Built-in (hickory-server)</td>
<td>Docker DNS + static IPs in exports.sh</td>
<td class="notes">StartOS has the most integrated DNS. Archipelago uses standard tools. Umbrel hardcodes IPs.</td>
</tr>
<tr><td colspan="5" class="compare-row-label">Identity & Web5</td></tr>
<tr>
<td class="cmp-label">DID Support</td>
<td class="good">did:key + did:dht + W3C DID Documents</td>
<td class="bad">None</td>
<td class="bad">None</td>
<td class="notes">Archipelago is the only node OS with decentralized identity support. Enables credential issuance and cross-node trust.</td>
</tr>
<tr>
<td class="cmp-label">Verifiable Credentials</td>
<td class="good">W3C VC 2.0 (Ed25519Signature2020)</td>
<td class="bad">None</td>
<td class="bad">None</td>
<td class="notes">Archipelago can issue and verify digital certificates without any central authority.</td>
</tr>
<tr>
<td class="cmp-label">DWN (Data Store)</td>
<td class="good">Custom implementation + peer sync via Tor</td>
<td class="bad">None</td>
<td class="bad">None</td>
<td class="notes">Personal data store that syncs across nodes. Unique to Archipelago.</td>
</tr>
<tr>
<td class="cmp-label">Nostr Integration</td>
<td class="good">NIP-01/04/44, nostr-provider.js in iframes</td>
<td class="bad">None</td>
<td class="bad">None</td>
<td class="notes">Archipelago injects Nostr identity into every app iframe for seamless decentralized social integration.</td>
</tr>
<tr><td colspan="5" class="compare-row-label">Infrastructure</td></tr>
<tr>
<td class="cmp-label">Base OS</td>
<td class="good">Debian 12 Bookworm (stable)</td>
<td class="good">Debian Bookworm</td>
<td class="warn">Debian Trixie (testing)</td>
<td class="notes">Stable: proven, security patches. Testing: newer packages but less battle-tested, potential for regressions.</td>
</tr>
<tr>
<td class="cmp-label">Filesystem</td>
<td>ext4</td>
<td class="good">btrfs (COW snapshots)</td>
<td>ext4 (A/B partitions)</td>
<td class="notes">btrfs snapshots enable instant rollback on failed installs. ext4 is simpler and more mature. A/B adds OS-level rollback.</td>
</tr>
<tr>
<td class="cmp-label">Kiosk Display</td>
<td class="good">X11 + Chromium on VT7</td>
<td class="bad">None</td>
<td class="bad">None</td>
<td class="notes">Plug in a monitor and the dashboard appears fullscreen. Unique physical UX for dedicated hardware.</td>
</tr>
<tr>
<td class="cmp-label">Boot Recovery</td>
<td>Crash recovery + container state snapshots</td>
<td>btrfs snapshots + preinit/postinit hooks</td>
<td class="warn">Destroys all containers on every boot</td>
<td class="notes">Archipelago/StartOS: resume from last known state. Umbrel: clean slate every boot (slower but deterministic).</td>
</tr>
</table>
</div>
<h3 class="subsection-title" style="margin-top: 24px;">Summary</h3>
<div class="sec-grid">
<div class="sec-card" style="border-color: rgba(99,102,241,0.2);">
<div class="sec-title" style="color: #a5b4fc;">Archipelago</div>
<div class="sec-desc"><b>Strengths:</b> Security (rootless, LUKS, caps, rate limiting, CSP), identity (DIDs, VCs, DWN, Nostr), kiosk display, Tor first-class.<br><b>Trade-offs:</b> No OTA updates (ISO reflash), ext4 lacks snapshot rollback, smaller app ecosystem.</div>
</div>
<div class="sec-card" style="border-color: rgba(16,185,129,0.2);">
<div class="sec-title" style="color: #6ee7b7;">StartOS</div>
<div class="sec-desc"><b>Strengths:</b> Package signing (S9PK), btrfs snapshots, built-in reverse proxy (fewer moving parts), WireGuard VPN, multi-arch (x86/ARM/RISC-V).<br><b>Trade-offs:</b> Tor removed in v0.4, no identity system, Angular is heavier, LXC is less container-ecosystem-compatible.</div>
</div>
<div class="sec-card" style="border-color: rgba(139,92,246,0.2);">
<div class="sec-title" style="color: #c4b5fd;">umbrelOS</div>
<div class="sec-desc"><b>Strengths:</b> Easiest setup, A/B OTA updates with rollback, largest app ecosystem, Git-based app store (easy contributions), React UI polish.<br><b>Trade-offs:</b> No disk encryption, flat network (no isolation), rootful Docker, no TLS, no rate limiting, no security headers, Node.js backend.</div>
</div>
</div>
</div>
<script>
// ═══════════════════════════════════════════════════════════════
// TWO-LEVEL NAVIGATION: System Selector + Tab Navigation
// ═══════════════════════════════════════════════════════════════
let currentSystem = 'archy'
let currentTab = 'overview'
// System prefix map: maps system name to section ID prefix
const sysPrefix = {
archy: 'sec-',
start9: 'sec-start9-',
umbrelos: 'sec-umbrelos-',
comparison: 'sec-compare-'
}
// Tabs that exist per system (Archipelago has all, others may lack some)
const sysTabs = {
archy: ['overview','layers','containers','protocols','web5','boot','security','data','glossary'],
start9: ['overview','layers','containers','protocols','web5','boot','security','data','glossary'],
umbrelos: ['overview','layers','containers','protocols','web5','boot','security','data','glossary'],
comparison: ['overview']
}
function showSystem(sys) {
currentSystem = sys
// Update system selector buttons
document.querySelectorAll('.sys-btn').forEach(b => b.classList.remove('active'))
const sysBtn = document.querySelector(`.sys-btn[data-system="${sys}"]`)
if (sysBtn) sysBtn.classList.add('active')
// Show/hide nav bar for comparison mode
const nav = document.querySelector('.nav')
if (sys === 'comparison') {
nav.style.display = 'none'
} else {
nav.style.display = 'flex'
}
// Navigate to current tab (or overview if tab doesn't exist for this system)
const tab = (sys === 'comparison') ? 'overview' : currentTab
showSection(tab)
}
function showSection(id) {
currentTab = id
// Hide all sections
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'))
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'))
// Resolve section ID based on current system
let secId
if (currentSystem === 'comparison') {
secId = 'sec-compare-overview'
} else if (id === 'glossary') {
secId = 'sec-glossary' // Glossary is shared
} else if (currentSystem === 'archy') {
secId = 'sec-' + id
} else {
secId = 'sec-' + currentSystem + '-' + id
}
const sec = document.getElementById(secId)
if (sec) {
sec.classList.add('active')
} else if (currentSystem !== 'archy') {
// Fallback: try Archipelago section if system-specific doesn't exist
const fallback = document.getElementById('sec-' + id)
if (fallback) fallback.classList.add('active')
}
const btn = document.querySelector(`.nav-btn[data-section="${id}"]`)
if (btn) btn.classList.add('active')
}
// System selector click handlers
document.querySelectorAll('.sys-btn').forEach(btn => {
btn.addEventListener('click', () => showSystem(btn.dataset.system))
})
// Tab click handlers
document.querySelectorAll('.nav-btn').forEach(btn => {
btn.addEventListener('click', () => showSection(btn.dataset.section))
})
// Card interactions
function toggle(el) {
// Toggle all cards in the same tier section together for side-by-side comparison
const tier = el.closest('.tier-section')
if (tier) {
const isExpanding = !el.classList.contains('expanded')
const cardsGrid = tier.querySelector('.cards')
const cards = tier.querySelectorAll('.card')
if (isExpanding) {
normalizeDetailRows(cards)
cards.forEach(c => c.classList.add('expanded'))
if (cardsGrid) cardsGrid.classList.add('all-expanded')
// Double-rAF ensures display:block has propagated before measuring
requestAnimationFrame(() => requestAnimationFrame(() => syncRowHeights(Array.from(cards))))
} else {
cards.forEach(c => c.classList.remove('expanded'))
if (cardsGrid) cardsGrid.classList.remove('all-expanded')
// Clear forced heights
tier.querySelectorAll('.detail-row, .card-desc, .card-layman, .card-image, .card-ports').forEach(r => r.style.minHeight = '')
}
} else {
el.classList.toggle('expanded')
}
}
// Collect all unique labels across cards in order, then ensure every card has every row
function normalizeDetailRows(cards) {
// Collect ordered union of all labels
const allLabels = []
const seen = new Set()
cards.forEach(card => {
const details = card.querySelector('.card-details')
if (!details) return
details.querySelectorAll('.detail-row').forEach(row => {
const label = row.querySelector('.detail-label')
if (label && !seen.has(label.textContent)) {
seen.add(label.textContent)
allLabels.push(label.textContent)
}
})
})
if (allLabels.length === 0) return
// For each card, build a map of existing rows and reorder + fill gaps
cards.forEach(card => {
const details = card.querySelector('.card-details')
if (!details) return
if (details.dataset.normalized) return // already done
details.dataset.normalized = '1'
const existing = {}
details.querySelectorAll('.detail-row').forEach(row => {
const label = row.querySelector('.detail-label')
if (label) existing[label.textContent] = row
})
// Rebuild in the canonical order
const frag = document.createDocumentFragment()
allLabels.forEach(label => {
if (existing[label]) {
frag.appendChild(existing[label])
} else {
// Create placeholder row
const row = document.createElement('div')
row.className = 'detail-row'
row.innerHTML = '<span class="detail-label">' + label + '</span><span class="detail-value" style="color:#3b3f50;">\u2014</span>'
frag.appendChild(row)
}
})
details.appendChild(frag)
})
}
// Synchronize rendered heights of matching elements across cards
function syncRowHeights(cards) {
if (cards.length < 2) return
// Helper: sync elements by class name across cards
function syncByClass(cls) {
const els = cards.map(c => c.querySelector('.' + cls)).filter(Boolean)
if (els.length < 2) return
els.forEach(e => e.style.minHeight = '')
let max = 0
els.forEach(e => { max = Math.max(max, e.offsetHeight) })
if (max > 0) els.forEach(e => e.style.minHeight = max + 'px')
}
// Sync every visible element so detail rows start at the same Y position
syncByClass('card-header')
syncByClass('card-desc')
syncByClass('card-layman')
syncByClass('card-image')
syncByClass('card-ports')
// Sync detail rows by position index
const maxRows = Math.max(...cards.map(c => c.querySelectorAll('.detail-row').length))
for (let i = 0; i < maxRows; i++) {
const rows = cards.map(c => {
const all = c.querySelectorAll('.detail-row')
return all[i] || null
}).filter(Boolean)
rows.forEach(r => r.style.minHeight = '')
let maxH = 0
rows.forEach(r => { maxH = Math.max(maxH, r.offsetHeight) })
if (maxH > 0) rows.forEach(r => r.style.minHeight = maxH + 'px')
}
}
function toggleProto(el) {
const grid = el.closest('.proto-grid')
if (grid) {
const isExpanding = !el.classList.contains('expanded')
const cards = Array.from(grid.querySelectorAll('.proto-card'))
if (isExpanding) {
// Convert free-form <b>Label:</b> HTML into structured detail-rows
cards.forEach(structureProtoDetails)
// Normalize so all cards have the same rows in same order
normalizeProtoRows(cards)
cards.forEach(c => c.classList.add('expanded'))
grid.classList.add('all-expanded')
requestAnimationFrame(() => requestAnimationFrame(() => syncProtoHeights(cards)))
} else {
cards.forEach(c => c.classList.remove('expanded'))
grid.classList.remove('all-expanded')
grid.querySelectorAll('.proto-card-name, .proto-card-desc, .proto-card-layman, .detail-row').forEach(e => e.style.minHeight = '')
}
} else {
el.classList.toggle('expanded')
}
}
// Convert <b>Label:</b> value<br> into structured detail-rows
function structureProtoDetails(card) {
const details = card.querySelector('.proto-details')
if (!details || details.dataset.structured) return
details.dataset.structured = '1'
const html = details.innerHTML
// Split on <br> or <br/> or <br />
const lines = html.split(/<br\s*\/?>/).map(l => l.trim()).filter(Boolean)
const rows = []
let currentLabel = null
let currentValue = ''
lines.forEach(line => {
// Check if line starts with <b>Label:</b>
const match = line.match(/^<b>([^<]+?):?<\/b>\s*(.*)/)
if (match) {
if (currentLabel) rows.push({ label: currentLabel, value: currentValue.trim() })
currentLabel = match[1].replace(/:$/, '').trim()
currentValue = match[2] || ''
} else if (currentLabel) {
// Continuation line (like bullet points)
currentValue += (currentValue ? '<br>' : '') + line
} else {
// Standalone line without label
currentLabel = line.replace(/<[^>]+>/g, '').substring(0, 20).trim()
currentValue = line
}
})
if (currentLabel) rows.push({ label: currentLabel, value: currentValue.trim() })
if (rows.length > 0) {
details.innerHTML = rows.map(r =>
'<div class="detail-row"><span class="detail-label">' + r.label + '</span><span class="detail-value">' + (r.value || '\u2014') + '</span></div>'
).join('')
}
}
// Normalize proto detail rows across cards (same as normalizeDetailRows)
function normalizeProtoRows(cards) {
const allLabels = []
const seen = new Set()
cards.forEach(card => {
const details = card.querySelector('.proto-details')
if (!details) return
details.querySelectorAll('.detail-row .detail-label').forEach(lbl => {
const t = lbl.textContent.trim()
if (!seen.has(t)) { seen.add(t); allLabels.push(t) }
})
})
if (allLabels.length === 0) return
cards.forEach(card => {
const details = card.querySelector('.proto-details')
if (!details || details.dataset.normalized) return
details.dataset.normalized = '1'
const existing = {}
details.querySelectorAll('.detail-row').forEach(row => {
const lbl = row.querySelector('.detail-label')
if (lbl) existing[lbl.textContent.trim()] = row
})
const frag = document.createDocumentFragment()
allLabels.forEach(label => {
if (existing[label]) {
frag.appendChild(existing[label])
} else {
const row = document.createElement('div')
row.className = 'detail-row'
row.innerHTML = '<span class="detail-label">' + label + '</span><span class="detail-value" style="color:#3b3f50;">\u2014</span>'
frag.appendChild(row)
}
})
details.appendChild(frag)
})
}
function syncProtoHeights(cards) {
if (cards.length < 2) return
function syncByClass(cls) {
const els = cards.map(c => c.querySelector('.' + cls)).filter(Boolean)
if (els.length < 2) return
els.forEach(e => e.style.minHeight = '')
let max = 0
els.forEach(e => { max = Math.max(max, e.offsetHeight) })
if (max > 0) els.forEach(e => e.style.minHeight = max + 'px')
}
syncByClass('proto-card-name')
syncByClass('proto-card-desc')
syncByClass('proto-card-layman')
// Sync detail rows by index
const maxRows = Math.max(...cards.map(c => c.querySelectorAll('.detail-row').length))
for (let i = 0; i < maxRows; i++) {
const rows = cards.map(c => {
const all = c.querySelectorAll('.detail-row')
return all[i] || null
}).filter(Boolean)
rows.forEach(r => r.style.minHeight = '')
let maxH = 0
rows.forEach(r => { maxH = Math.max(maxH, r.offsetHeight) })
if (maxH > 0) rows.forEach(r => r.style.minHeight = maxH + 'px')
}
}
function find(name) {
event.stopPropagation()
const card = document.querySelector(`[data-name="${name}"]`)
if (card) {
showSystem('archy')
showSection('containers')
document.querySelectorAll('#sec-containers .filter-btn').forEach(b => b.classList.remove('active'))
document.querySelector('#sec-containers [data-filter="all"]').classList.add('active')
document.querySelectorAll('#sec-containers .card').forEach(c => c.classList.remove('hidden'))
document.querySelectorAll('#sec-containers .tier-section').forEach(t => t.classList.remove('hidden'))
setTimeout(() => {
card.scrollIntoView({ behavior: 'smooth', block: 'center' })
card.classList.add('expanded', 'highlight')
setTimeout(() => card.classList.remove('highlight'), 1200)
}, 100)
}
}
function showContainers(name) {
showSystem('archy')
showSection('containers')
setTimeout(() => find(name), 50)
}
// Container filters
document.querySelectorAll('#sec-containers .filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#sec-containers .filter-btn').forEach(b => b.classList.remove('active'))
btn.classList.add('active')
const filter = btn.dataset.filter
document.querySelectorAll('#sec-containers .card').forEach(card => {
if (filter === 'all') { card.classList.remove('hidden'); return }
const tierMatch = card.dataset.tier && card.dataset.tier.includes(filter)
const netMatch = card.dataset.net === filter
card.classList.toggle('hidden', !(tierMatch || netMatch))
})
document.querySelectorAll('#sec-containers .tier-section').forEach(section => {
if (filter === 'all' || filter === 'archy-net' || filter === 'bridge') {
section.classList.remove('hidden')
return
}
const tier = section.dataset.tier
section.classList.toggle('hidden', tier !== filter)
})
})
})
// ═══════════════════════════════════════════════════════════════
// KEYWORD POPOVER SYSTEM
// ═══════════════════════════════════════════════════════════════
const GLOSSARY = {
// Bitcoin & Lightning
"Bitcoin Knots": { tech: "A Bitcoin Core fork by Luke Dashjr with enhanced policy controls and spam filtering. Full node implementation that validates every block and transaction.", plain: "Your own copy of Bitcoin's entire ledger. It checks every transaction independently — you trust math, not people.", used: "Container: bitcoin-knots on port 8332/8333" },
"Bitcoin Core": { tech: "The reference implementation of the Bitcoin protocol. Maintains the UTXO set, validates blocks, and participates in the P2P gossip network.", plain: "The original Bitcoin software. It downloads the entire blockchain and verifies every transaction since 2009.", used: "bitcoin-knots is a fork of Bitcoin Core" },
"full node": { tech: "Software that fully validates every transaction and block against consensus rules without trusting any third party.", plain: "A computer that checks every Bitcoin transaction itself. If someone tries to cheat, your node catches it and rejects it." },
"Lightning Network": { tech: "Layer 2 payment channel network using HTLCs (Hash Time-Locked Contracts) for instant, low-fee routed payments on top of Bitcoin.", plain: "A fast-payment system built on Bitcoin. Open a 'tab' (channel) with some Bitcoin, then send thousands of instant payments through it. Settles in milliseconds, costs fractions of a penny." },
"LND": { tech: "Lightning Network Daemon — the most popular Lightning implementation. Exposes gRPC (port 10009) and REST (port 8080) APIs. Auth via macaroon bearer tokens.", plain: "The software that runs your Lightning node. Manages payment channels and routes payments across the network.", used: "Container: lnd on ports 9735/10009/8080" },
"Lightning": { tech: "Bitcoin's Layer 2 payment channel network. Uses bidirectional channels with HTLC routing (BOLT specification).", plain: "Instant Bitcoin payments. Two people deposit Bitcoin into a shared channel, then send money back and forth instantly without waiting for blockchain confirmations." },
"payment channel": { tech: "A 2-of-2 multisig Bitcoin transaction that enables off-chain payments between two parties, settled on-chain only when closed.", plain: "Like a bar tab — you deposit some Bitcoin, make unlimited fast payments, and only settle on the blockchain when you close the tab." },
"ElectrumX": { tech: "An Electrum protocol server that indexes the Bitcoin blockchain by address. Enables fast UTXO and transaction history lookups on port 50001.", plain: "An address lookup index for Bitcoin. Without it, wallet apps can't quickly check balances. With it, you get instant results from YOUR node instead of trusting a public server.", used: "Container: electrumx on port 50001" },
"Mempool": { tech: "Open-source block explorer (mempool.space). Visualizes pending transactions, fee estimates, block history. Requires: bitcoin-knots + electrumx + mariadb.", plain: "A visual dashboard for the Bitcoin blockchain. See pending transactions, fee rates, recent blocks — all from your own node, completely private.", used: "Containers: mempool-api (8999), archy-mempool-web (4080), archy-mempool-db" },
"BTCPay": { tech: "Self-hosted Bitcoin payment processor. Generates invoices, tracks payments, supports Lightning. Uses NBXplorer for chain indexing + PostgreSQL for storage.", plain: "Your own payment processing system — like Stripe or PayPal but for Bitcoin. Accept payments without middlemen or processing fees.", used: "Container: btcpay-server on port 23000" },
"BTCPay Server": { tech: "Self-hosted Bitcoin payment processor. Generates invoices, tracks payments, supports Lightning. Uses NBXplorer for chain indexing + PostgreSQL for storage.", plain: "Your own payment processing system — like Stripe or PayPal but for Bitcoin. Accept payments without middlemen or processing fees.", used: "Container: btcpay-server on port 23000" },
"NBXplorer": { tech: "Lightweight blockchain indexer for BTCPay. Tracks wallet addresses and UTXOs. Connects to Bitcoin Core via RPC.", plain: "Watches the blockchain for payments to your BTCPay addresses. When a customer pays, it tells BTCPay immediately.", used: "Container: archy-nbxplorer on port 32838" },
"Fedimint": { tech: "Federated e-cash protocol. Multi-party (m-of-n) custody with Chaumian blind-signed tokens for transaction privacy. Guardians hold BTC in multisig.", plain: "A community Bitcoin bank. A group of trusted people hold Bitcoin together and issue private digital cash to members. Nobody — not even the guardians — can see who pays whom.", used: "Containers: fedimint (8173-8175), fedimint-gateway (8176)" },
"ecash": { tech: "Chaumian blind-signed bearer tokens backed by Bitcoin in a Fedimint federation. Unlinkable — the mint cannot trace token transfers.", plain: "Digital cash that's truly private. Like physical bills — the bank that issued them can't track where they go. Backed 1:1 by real Bitcoin." },
"UTXO": { tech: "Unspent Transaction Output — Bitcoin's accounting model. Coins are tracked as discrete outputs, not account balances. Each UTXO can only be spent once.", plain: "Bitcoin doesn't have 'accounts' with balances. Instead, it tracks individual coins (outputs). Your balance is the sum of all your unspent coins." },
"BIP-39": { tech: "Bitcoin Improvement Proposal 39 — standard for encoding wallet seeds as 12 or 24 human-readable words (mnemonic phrase). Enables wallet recovery.", plain: "The 12 or 24 words that back up your Bitcoin wallet. From these words, all your keys and addresses can be regenerated. Write them down and keep them safe." },
"seed phrase": { tech: "BIP-39 mnemonic — 12 or 24 words encoding a master key from which all Bitcoin keys are derived via BIP-32 hierarchical deterministic derivation.", plain: "The master backup for your Bitcoin wallet. 12 or 24 ordinary English words that can recreate all your Bitcoin keys." },
"PSBT": { tech: "Partially Signed Bitcoin Transaction (BIP-174). A standardized format for constructing transactions that can be signed by multiple parties or hardware wallets.", plain: "A way to build a Bitcoin transaction in steps. Create it on one device, sign it on a hardware wallet, broadcast from another. Multi-step security." },
"txindex": { tech: "Transaction index — a database that maps every transaction ID to its location in the blockchain. Enables lookups by txid. Requires full (non-pruned) blockchain.", plain: "A search index that lets you look up any Bitcoin transaction instantly by its ID. Only works with the full blockchain (not pruned)." },
"pruning": { tech: "Discarding old block data after validation. Saves disk space while still fully validating the blockchain. Bitcoin Core prunes blocks older than a configurable threshold.", plain: "Your node validates every transaction but deletes the old raw data afterwards to save disk space. The validation still happened — you just don't keep a copy of every old block." },
"rpcauth": { tech: "Bitcoin Core authentication method using salted HMAC-SHA256. The password hash is stored in bitcoin.conf, never the plaintext password.", plain: "How the backend securely authenticates to Bitcoin Core. The password is stored as a mathematical hash — never in readable form." },
"multisig": { tech: "Multi-signature — requires M of N keys to authorize a transaction (e.g., 3-of-5). Used by Fedimint guardians and advanced wallets.", plain: "A Bitcoin vault that needs multiple keys to open — like a safe deposit box needing 3 out of 5 people to agree before funds can move." },
// Infrastructure & Containers
"Podman": { tech: "Daemonless, rootless container engine. Drop-in Docker replacement. Runs containers as unprivileged user. Uses NetAvark for networking.", plain: "The engine that runs all the app containers. Like Docker but more secure — doesn't need administrator privileges, so a compromised container can't take over the machine.", used: "Runs as user 'archipelago' (UID 1000)" },
"container": { tech: "An isolated process with its own filesystem, network namespace, and resource limits. Shares the host kernel. Managed by Podman using OCI images.", plain: "A sealed room for running one app. Each container has its own files and network, can't see other containers, and has memory limits. If one crashes, the others keep running." },
"rootless": { tech: "Containers running as unprivileged user (UID 1000) with user namespace mapping. Even container root (UID 0) maps to unprivileged host UID.", plain: "Containers run as a regular user, not as administrator. Even if an attacker breaks out of a container, they're just a normal user — not root." },
"archy-net": { tech: "A Podman bridge network connecting all Bitcoin-stack containers. Provides DNS resolution by container name (e.g., 'bitcoin-knots' resolves to its IP).", plain: "A private network connecting Bitcoin-related containers so they can find each other by name. Isolated from standalone app containers.", used: "Members: bitcoin-knots, electrumx, lnd, mempool-*, btcpay-*, fedimint-*" },
"bridge network": { tech: "A virtual L2 network segment (NetAvark). Containers on the same bridge get DNS resolution and can communicate. Isolated from other bridges.", plain: "A virtual cable connecting containers together. Containers on the same bridge can talk to each other using names instead of IP addresses." },
"Nginx": { tech: "High-performance reverse proxy and web server. Listens on ports 80/443, terminates TLS, rate-limits requests, adds security headers, routes /app/* to containers.", plain: "The front door to your server. Every web request goes through Nginx, which figures out which app you want and forwards you there. Also handles HTTPS encryption and blocks bad traffic.", used: "Ports 80 (HTTP) + 443 (HTTPS)" },
"reverse proxy": { tech: "A server that accepts client requests and forwards them to backend services. Handles TLS termination, load balancing, caching, and security headers.", plain: "A receptionist that directs visitors to the right office. You go to one address (your server) and the proxy figures out which app you want based on the URL." },
"systemd": { tech: "Linux init system (PID 1) and service manager. Manages service lifecycle, dependencies, restart policies, resource limits, sandboxing, and timers.", plain: "The conductor of the orchestra. It starts everything in the right order, restarts things that crash, and runs scheduled maintenance tasks." },
"Debian": { tech: "Debian 12 (Bookworm) — stable Linux distribution. Minimal debootstrap install with only required packages.", plain: "The operating system under everything. Chosen for rock-solid stability and long-term security updates." },
// Networking & Privacy
"Tor": { tech: "The Onion Router — anonymity network routing traffic through 3+ relay hops. Creates .onion hidden services for inbound connectivity without IP exposure.", plain: "Routes your traffic through multiple random computers worldwide so nobody can trace it back to you. Creates secret .onion addresses for your services.", used: "Hidden services for: UI, Bitcoin P2P, ElectrumX, LND, BTCPay" },
"hidden service": { tech: "A Tor service accessible via a .onion address. Uses rendezvous-based routing so the server's IP is never revealed. Keys stored on encrypted partition.", plain: "A secret address for your server that works globally without revealing where it physically is. Like a PO Box that forwards to your house without giving out your address." },
".onion": { tech: "Tor hidden service address — a 56-character Base32 string derived from the service's Ed25519 public key. Accessed only through the Tor network.", plain: "A Tor address — a long string of characters ending in .onion. It's your server's anonymous identity on the Tor network." },
"Tailscale": { tech: "Zero-config mesh VPN built on WireGuard. Peer-to-peer encrypted tunnels with NAT traversal. DERP relay fallback.", plain: "A modern VPN that connects your devices into a private network. Access your server from your phone anywhere in the world. No port forwarding needed.", used: "Container: tailscale on host network" },
"WireGuard": { tech: "Modern VPN protocol. UDP-based, ~4000 lines of code. ChaCha20-Poly1305 encryption, Curve25519 key exchange. Minimal attack surface.", plain: "A fast, simple VPN protocol used by Tailscale. Much faster and more secure than older VPN technologies." },
"TLS": { tech: "Transport Layer Security — encrypts HTTP traffic (HTTPS). Archipelago uses a self-signed certificate (2048-bit RSA, 10-year validity).", plain: "The padlock icon in your browser. Encrypts the connection between your browser and the server so nobody on the network can eavesdrop." },
"HTTPS": { tech: "HTTP over TLS. Nginx listens on port 443 with a self-signed certificate. HSTS header forces future visits to use HTTPS.", plain: "Encrypted web traffic. The 'S' in HTTPS means your connection is scrambled — nobody between you and the server can read it." },
// Protocols
"JSON-RPC": { tech: "Remote Procedure Call encoded as JSON over HTTP POST. Request: {method, params}. Response: {result} or {error}. Single endpoint: /rpc/v1.", plain: "A simple way for the dashboard to ask the backend to do things. Send 'call system.stats', get back the numbers. All through one URL.", used: "194+ methods at POST /rpc/v1" },
"WebSocket": { tech: "Full-duplex TCP connection (RFC 6455). Server pushes JSON Patch (RFC 6902) deltas. 30s ping, 5min timeout. Endpoint: /ws/db.", plain: "A permanent open phone line between your browser and the server. Instead of constantly asking 'what changed?', the server tells you instantly when something happens." },
"gRPC": { tech: "Google's RPC framework using HTTP/2 + Protocol Buffers binary serialization. Bidirectional streaming. Much faster than JSON-based APIs.", plain: "A high-speed way for programs to talk to each other. Uses compact binary format instead of text. The backend uses it to control LND.", used: "Backend → LND on port 10009" },
"JSON Patch": { tech: "RFC 6902 — a format for describing changes to a JSON document. Operations: add, remove, replace, move, copy. Used by WebSocket for incremental state updates.", plain: "Instead of sending the entire system state every time something changes, the server sends just the tiny piece that changed. Like saying 'change CPU to 42%' instead of resending everything." },
"CBOR": { tech: "Concise Binary Object Representation (RFC 8949). Like JSON but binary-encoded. 2-5x smaller. Used for mesh radio messages where bandwidth is precious.", plain: "A compact way to pack data. Critical for radio communication where you can only send a few bytes per second." },
// Security
"LUKS": { tech: "Linux Unified Key Setup v2. Full-disk encryption with AES-256-XTS (AES-NI) or ChaCha20-Adiantum fallback. argon2id key derivation.", plain: "The lock on your data partition. Scrambles everything so only your machine can read it. Uses the best encryption your CPU supports.", used: "Partition 4 → /var/lib/archipelago/" },
"LUKS2": { tech: "Linux Unified Key Setup v2. Full-disk encryption with AES-256-XTS (AES-NI) or ChaCha20-Adiantum fallback. argon2id key derivation.", plain: "The lock on your data partition. Scrambles everything so only your machine can read it. Uses the best encryption your CPU supports.", used: "Partition 4 → /var/lib/archipelago/" },
"AES-NI": { tech: "Advanced Encryption Standard New Instructions — CPU hardware acceleration for AES operations. Available on most x86 CPUs since 2010.", plain: "A feature in modern processors that makes encryption extremely fast — almost free in terms of CPU usage." },
"argon2id": { tech: "Memory-hard password hashing function. Resists GPU/ASIC brute-force by requiring significant RAM during key derivation.", plain: "A password-strengthening technique that's intentionally slow and memory-hungry. Makes it impractical for attackers to guess your encryption key even with powerful hardware." },
"RBAC": { tech: "Role-Based Access Control. Three roles: Admin (all 194+ methods), Viewer (read-only subset), AppUser (minimal). Explicit allowlist per role, not deny list.", plain: "A permission system. Admins can do everything. Viewers can only look. AppUsers get limited features. Each role has an explicit list of what it can do." },
"CSRF": { tech: "Cross-Site Request Forgery — attack where malicious site triggers actions on your server. Mitigated with HMAC-SHA256 token derived from session, sent in X-CSRF-Token header.", plain: "A trick where a bad website tries to make your browser do things on your server. The CSRF token is a secret code that proves the request comes from the real dashboard." },
"macaroon": { tech: "Capability-based bearer token with embedded caveats (restrictions). Used by LND. Can be attenuated (restricted further) without server involvement.", plain: "A permission slip for Lightning. The admin macaroon can do everything; you can create a read-only version that only checks balances. Like making a key that only opens one door." },
"rate limiting": { tech: "Per-IP sliding window throttle on sensitive endpoints. Financial ops: 3-10 per 5min. Auth: 5 failures per 60s. Federation: 5-30 per 60s.", plain: "Prevents abuse by limiting how often you can do something. Login: 5 tries per minute. Send Bitcoin: 5 times per 5 minutes. Stops brute-force attacks cold." },
// Identity & Social
"DID": { tech: "Decentralized Identifier (W3C standard). A URI that resolves to a DID Document containing public keys and service endpoints. No central registry needed.", plain: "A digital identity you create and control yourself. No company can delete it, no platform can ban it. Like a self-issued passport backed by cryptography." },
"Nostr": { tech: "Notes and Other Stuff Transmitted by Relays. Decentralized social protocol using Ed25519-signed events over WebSocket. NIP standards define features.", plain: "A decentralized social media protocol. You sign your posts with your cryptographic key, publish to relay servers that anyone can run. No single company controls the network." },
"Verifiable Credentials": { tech: "W3C VC standard. JSON-LD format with Ed25519 proof. Cryptographically signed claims that can be verified without contacting the issuer.", plain: "Digital proof that someone vouches for something. Like a digitally signed certificate — anyone can verify it's real without calling the issuer." },
"NIP-04": { tech: "Nostr Implementation Possibility 04 — deprecated encrypted direct message standard using shared ECDH key.", plain: "An older way to send private messages on Nostr. Being replaced by NIP-44 which is more secure." },
"NIP-44": { tech: "Modern Nostr encrypted DM standard with forward secrecy and padding. Uses XChaCha20-Poly1305.", plain: "The new, more secure way to send private messages on Nostr." },
// Mesh & Federation
"federation": { tech: "A cluster of Archipelago nodes with trust relationships. Nodes share state, deploy apps, and communicate over Tor. Three trust levels: Trusted, Observer, Untrusted.", plain: "A team of Archipelago servers that work together. They share health status, can install apps on each other, and form a resilient network." },
"LoRa": { tech: "Long Range radio — sub-GHz ISM band. Low bandwidth (~300 bps), long range (5-15 km). Mesh topology with store-and-forward.", plain: "A type of radio that sends small messages over very long distances (up to 15 km) without internet or cell towers. For truly off-grid communication." },
"mesh networking": { tech: "Peer-to-peer radio network where nodes relay messages for each other. Uses CBOR encoding, X3DH handshake, and Double Ratchet encryption.", plain: "Nodes talk to each other over radio and relay messages for each other, extending range. Like a bucket brigade — each node passes messages along until they reach the destination." },
"Double Ratchet": { tech: "Forward-secret key agreement protocol (Signal). Per-message key derivation from a ratcheting chain. Compromising one key cannot decrypt past messages.", plain: "The encryption behind Signal. Every message gets a unique key, and old keys are destroyed. Even if someone steals your key tomorrow, they can't read yesterday's messages." },
"X3DH": { tech: "Extended Triple Diffie-Hellman — key agreement protocol for establishing encrypted sessions. Used to bootstrap Double Ratchet sessions.", plain: "A secure handshake. Two devices exchange math problems to agree on a secret key without ever sending the key itself. The starting point for encrypted messaging." },
// Apps
"Vaultwarden": { tech: "Lightweight Bitwarden-compatible password manager server. Rust implementation. Stores encrypted vaults locally.", plain: "Self-hosted password manager. Compatible with all Bitwarden apps (phone, browser extension). Your passwords are stored on YOUR server, not someone else's cloud.", used: "Container: vaultwarden on port 8082" },
"Nextcloud": { tech: "Self-hosted productivity suite — file sync (WebDAV), calendar (CalDAV), contacts (CardDAV), document editing, and collaboration.", plain: "Your own Google Drive + Google Calendar + Google Contacts. Sync files across devices, share folders, edit documents. All on your hardware.", used: "Container: nextcloud on port 8085" },
"Jellyfin": { tech: "Open-source media server. Organizes and streams video/audio/photo libraries. DLNA support. No account or subscription required.", plain: "Self-hosted Netflix. Organize and stream your movies, TV shows, and music to any device. Free, no tracking, no subscriptions.", used: "Container: jellyfin on port 8096" },
"PhotoPrism": { tech: "AI-powered photo management with TensorFlow-based object detection, face recognition, and EXIF-based organization.", plain: "AI-powered photo organizer. It automatically tags, categorizes, and makes your photos searchable — like Google Photos but running on your hardware.", used: "Container: photoprism on port 2342" },
"Grafana": { tech: "Monitoring and visualization platform. Dashboards for time-series data. Supports Prometheus, InfluxDB, and other data sources.", plain: "Beautiful real-time graphs showing how your server is doing — CPU usage, disk space, Bitcoin sync progress, container health.", used: "Container: grafana on port 3000" },
"Home Assistant": { tech: "Home automation platform. Supports 2000+ IoT device integrations. Local processing, no cloud dependency.", plain: "Smart home control center. Control lights, thermostats, cameras, and sensors from one interface — all running locally, no Amazon/Google cloud needed.", used: "Container: homeassistant on port 8123" },
"SearXNG": { tech: "Privacy-respecting metasearch engine. Proxies queries to 70+ search engines without revealing user identity.", plain: "Private search engine. It searches Google, Bing, and DuckDuckGo on your behalf without them knowing who you are.", used: "Container: searxng on port 8888" },
"OnlyOffice": { tech: "Document server for collaborative editing of Office formats (.docx, .xlsx, .pptx). Integrates with Nextcloud.", plain: "Like Google Docs but self-hosted. Edit Word, Excel, and PowerPoint files right in your browser.", used: "Container: onlyoffice on port 9980" },
"Ollama": { tech: "Local LLM inference engine. Runs quantized models (Llama, Mistral, etc.) on CPU or GPU. REST API on port 11434.", plain: "Run AI chatbots locally on your hardware. Your prompts never leave your machine — truly private AI.", used: "Container: ollama on port 11434" },
"Uptime Kuma": { tech: "Self-hosted monitoring tool. Pings services, tracks response times, sends alerts via webhook, email, or push notifications.", plain: "Checks if your services are online and alerts you when something goes down. Your own personal Pingdom.", used: "Container: uptime-kuma on port 3001" },
"FileBrowser": { tech: "Web-based file manager with upload/download, sharing, and user management. Serves the Cloud storage area.", plain: "Manage your files through a browser. Upload, download, and organize documents, photos, and media.", used: "Container: filebrowser on port 8083" },
"Portainer": { tech: "Container management web UI. Visualizes running containers, logs, networks, and volumes.", plain: "A visual dashboard for your containers. See what's running, check logs, restart things — without needing the command line.", used: "Container: portainer on port 9000" },
"Penpot": { tech: "Open-source design platform (Figma alternative). 5-container stack: frontend, backend, exporter, PostgreSQL, Valkey on penpot-net.", plain: "Design tool like Figma but self-hosted. Create UI mockups, graphics, and prototypes in your browser.", used: "Container stack on penpot-net, port 9001" },
"IndeedHub": { tech: "Decentralized media platform. 7-container stack: Next.js frontend, API, PostgreSQL, Redis, MinIO (S3), FFmpeg, Nostr relay.", plain: "Decentralized YouTube. Host and stream videos without centralized platforms. Uses Nostr for discovery and Bitcoin for monetization.", used: "Container stack on bridge, port 7777" },
"Immich": { tech: "Self-hosted photo/video backup with ML-based face recognition and object detection. 3-container stack with vector-enabled PostgreSQL.", plain: "Google Photos replacement with AI features. Automatically backs up and organizes your photos from your phone.", used: "Container stack on bridge, port 2283" },
"DWN": { tech: "Decentralized Web Node (W3C standard). Personal data store with DID-based access control. Stores structured data with protocol schemas.", plain: "A personal database that YOU own. Apps request permission to read/write data. You control access with your decentralized identity." },
// Infra
"MariaDB": { tech: "MySQL-compatible relational database. Fork of MySQL with enhanced performance. Used by Mempool for block/tx data.", plain: "A database that stores structured data in tables (like a powerful spreadsheet). Mempool uses it to cache blockchain data for fast lookups.", used: "Container: archy-mempool-db" },
"PostgreSQL": { tech: "Advanced relational database with ACID compliance, JSON support, and extensibility. Used by BTCPay, IndeedHub, Penpot, Immich.", plain: "A powerful database used by several apps to store their data reliably. Known for being rock-solid and feature-rich.", used: "Containers: archy-btcpay-db, indeedhub-postgres, penpot-postgres, immich_postgres" },
"Redis": { tech: "In-memory key-value store. Sub-millisecond reads. Used for caching, session storage, and message queuing.", plain: "A super-fast notepad that apps use to remember things temporarily. Instead of reading from disk, frequently needed data is kept in RAM.", used: "Container: indeedhub-redis" },
"Valkey": { tech: "Open-source Redis fork maintained by the Linux Foundation. API-compatible drop-in replacement.", plain: "An open-source clone of Redis. Functionally identical, just under a different license.", used: "Container: penpot-valkey, immich_redis" },
"MinIO": { tech: "S3-compatible object storage server. High-performance, supports multipart uploads and bucket policies.", plain: "Stores media files (videos, images) using the same API as Amazon S3 — but running on your hardware. Apps built for S3 work with it automatically.", used: "Container: indeedhub-minio" },
"GRUB": { tech: "GRand Unified Bootloader. Stage 2 bootloader supporting BIOS (i386-pc) and UEFI (x86_64-efi). Loads Linux kernel + initramfs.", plain: "The first program that runs when you turn on the computer. It shows a menu and loads the operating system. Works on both old and new hardware." },
"squashfs": { tech: "Compressed read-only filesystem. Used by the installer to pack the live environment into a single file on the ISO.", plain: "A compressed package of the installer's temporary operating system. Boots entirely from RAM." },
"GPT": { tech: "GUID Partition Table — modern disk partitioning scheme. Supports disks >2TB, up to 128 partitions. Used by Archipelago's auto-installer.", plain: "The modern way to divide a hard drive into sections. Supports large disks and is more reliable than the older MBR format." },
"debootstrap": { tech: "Tool that installs a Debian base system into a subdirectory. Used to build the minimal Archipelago root filesystem.", plain: "A tool that creates a minimal Debian system from scratch — only the packages you need, nothing extra." },
"ext4": { tech: "Fourth Extended Filesystem — the most common Linux filesystem. Journaled, supports files up to 16TB.", plain: "The standard filesystem used by Linux. Reliable, fast, and battle-tested on millions of servers." },
// Protocols & Crypto
"Ed25519": { tech: "Edwards-curve Digital Signature Algorithm using Curve25519. 128-bit security, fast signing/verification, small keys (32 bytes).", plain: "A modern digital signature method. Used to sign messages and prove identity. Very fast and secure." },
"HMAC-SHA256": { tech: "Hash-based Message Authentication Code using SHA-256. Provides message integrity and authentication.", plain: "A way to verify that a message hasn't been tampered with and came from the expected sender." },
"secp256k1": { tech: "Elliptic curve used by Bitcoin for public key cryptography. Also used by Nostr for event signing.", plain: "The mathematical curve that Bitcoin uses for its signatures. Also used by Nostr for signing social media posts." },
"Noise protocol": { tech: "Framework for building encrypted transport protocols. Used by Lightning P2P (port 9735) and WireGuard.", plain: "An encryption framework used by Lightning and WireGuard. Like TLS but simpler and more flexible." },
// Web5 / Identity
"did:key": { tech: "Self-resolving DID method where the public key is encoded directly in the identifier. Format: did:key:z6Mk... (multicodec Ed25519 in base58btc). No external resolution needed.", plain: "A digital identity that IS your cryptographic key. Like a self-addressed envelope — the identity carries its own proof. Works offline, instant, free." },
"did:dht": { tech: "DID method that publishes DID Documents to BitTorrent's Mainline DHT using BEP-44 signed mutable items. Format: did:dht:z... (z-base-32 encoded Ed25519 pubkey).", plain: "A digital identity published to the BitTorrent network. Anyone in the world can look you up without a central registry." },
"DWN": { tech: "Decentralized Web Node (W3C/DIF draft). Personal data store with protocol-governed records, DID-based auth, and peer sync. Implements Records interface (Write/Read/Query/Delete).", plain: "A personal database that YOU own. Apps request permission to read/write data. You control access with your decentralized identity." },
"Verifiable Credential": { tech: "W3C standard (VC Data Model 2.0). Cryptographically signed claims from an Issuer about a Subject. Includes proof (Ed25519Signature2020), expiration, and revocation status.", plain: "A digital certificate that proves something about you — signed by an issuer, verifiable by anyone without contacting the issuer. Like a digitally signed diploma." },
"Web5": { tech: "Decentralized web platform combining DIDs + DWNs + Verifiable Credentials. Initiated by TBD (Block/Jack Dorsey), shut down Nov 2024. Core specs (DID, VC) are W3C standards. Open source donated to DIF.", plain: "A vision for the decentralized web where you own your identity and data. Built on open standards, not tokens or blockchains." },
"Patch-DB": { tech: "Custom database used by StartOS. CBOR-encoded with diff-based reactive sync over WebSocket. Backend pushes deltas, frontend applies them in real-time.", plain: "StartOS's custom database that automatically syncs changes to the UI in real-time, sending only what changed instead of everything." },
// Comparison systems
"LXC": { tech: "Linux Containers — OS-level virtualization using kernel namespaces and cgroups. Full system containers (not app containers like Docker/Podman). Used by StartOS.", plain: "A way to run isolated Linux environments that look like full virtual machines but share the host kernel. Heavier than Docker but more isolated." },
"Docker": { tech: "Container platform using OCI images. Daemon-based (dockerd runs as root). Default for umbrelOS. Docker Compose v2 for multi-container apps.", plain: "The most popular container tool. Runs apps in isolated environments. Requires a background service running as root (unlike Podman which is rootless)." },
"tRPC": { tech: "TypeScript-first RPC framework. End-to-end type safety between server and client without code generation. Used by umbrelOS for its API.", plain: "A way for the frontend and backend to communicate with full type safety — if the API changes, TypeScript catches errors automatically." },
"S9PK": { tech: "StartOS package format v2. Signed merkle archive with Ed25519 signatures over blake3 roots. Contains: manifest.json, JavaScript service code (squashfs), container images per architecture, assets, icon.", plain: "StartOS's app package format. Each package is cryptographically signed and contains everything needed to run the app." },
"Rugix": { tech: "A/B partition update system used by umbrelOS (formerly Rugpi). Writes OS updates to inactive partition, swaps on reboot, automatic rollback if boot fails.", plain: "A safe OS update system. Updates install to a backup partition. If the update breaks, it automatically reverts to the working version." },
"btrfs": { tech: "B-tree filesystem with copy-on-write (COW), snapshots, checksums, and compression. Used by StartOS for safe app installs via instant volume snapshots.", plain: "A modern filesystem that can take instant snapshots of your data. StartOS uses this to safely roll back failed app updates." },
// Container & OS Technology
"OCI": { tech: "Open Container Initiative \u2014 industry standard for container image formats and runtimes. Docker, Podman, and containerd all use OCI images.", plain: "A universal standard for containers. Like how USB works with any device \u2014 OCI means containers work with any compatible runtime." },
"OverlayFS": { tech: "Union filesystem that layers a writable directory on top of a read-only base. Changes are stored in the upper layer without modifying the lower. Used by Docker and LXC.", plain: "A layered filesystem \u2014 like putting a transparent sheet over a picture. You can draw on it without changing the original underneath." },
"AppArmor": { tech: "Linux Mandatory Access Control (MAC) framework. Restricts what programs can do via per-application security profiles. Used by StartOS for LXC container isolation.", plain: "A security guard for each app. It has a rulebook saying exactly what the app is allowed to touch \u2014 files, network, devices \u2014 and blocks everything else." },
"Docker Compose": { tech: "Tool for defining and running multi-container Docker applications via YAML files. Each app becomes a 'project' with its own containers, networks, and volumes.", plain: "A recipe file for running multiple containers together. You describe what you need in YAML, and Compose creates and connects everything." },
"user namespaces": { tech: "Linux kernel feature that maps container UIDs to different host UIDs. Container root (UID 0) can map to an unprivileged host user (e.g., UID 100000), preventing privilege escalation.", plain: "A trick that makes container apps think they're running as admin, but they're actually a regular user on the host. If they escape, they have no real power." },
"UID mapping": { tech: "The translation table between container user IDs and host user IDs in user namespaces. E.g., container UID 0 \u2192 host UID 100000, container UID 70 \u2192 host UID 100070.", plain: "A translation table for user identities between inside and outside a container. Container 'root' becomes a nobody on the host machine." },
"capability dropping": { tech: "Linux security mechanism that removes specific privileges (capabilities) from processes. --cap-drop=ALL removes everything, then --cap-add adds back only what's needed.", plain: "Instead of giving an app all admin powers, you take away everything and only give back the specific abilities it actually needs. Like a hotel keycard that only opens your room." },
"veth": { tech: "Virtual Ethernet \u2014 a pair of connected virtual network interfaces. One end goes inside the container, the other connects to a bridge on the host. Used for container networking.", plain: "A virtual network cable connecting a container to the host. Like plugging an Ethernet cable between two computers, but entirely in software." },
"A/B partitions": { tech: "Dual root partition scheme for atomic OS updates. Updates install to the inactive partition; on reboot the system switches to it. If boot fails, it reverts to the previous partition.", plain: "Two copies of the operating system on disk. Updates go to the spare copy. If it works, great \u2014 if not, it automatically switches back to the good copy." },
"rootful": { tech: "Container runtime where the daemon runs as root (UID 0). Docker is rootful by default. If a container escape occurs, the attacker gains root access on the host.", plain: "The container manager runs with full admin access. Simpler to set up, but if something breaks out of a container, it has full control of the machine." },
// Web Standards & Certificates
"ACME": { tech: "Automatic Certificate Management Environment \u2014 protocol for automated TLS certificate issuance (used by Let's Encrypt). Verifies domain ownership via challenges (HTTP-01, DNS-01, TLS-ALPN-01).", plain: "A system that automatically gets and renews HTTPS certificates for free. Let's Encrypt uses it to prove you own a domain and issue a certificate." },
"SNI": { tech: "Server Name Indication \u2014 TLS extension where the client sends the requested hostname at the start of the handshake. Allows one server/IP to serve different certificates for different domains.", plain: "Tells the web server which website you want before the encrypted connection starts. This lets one server host multiple HTTPS sites on one IP address." },
"mDNS": { tech: "Multicast DNS \u2014 resolves hostnames on local networks without a DNS server. Devices announce themselves as name.local (e.g., archipelago.local). Implemented by Avahi on Linux, Bonjour on macOS.", plain: "How your phone finds your node on the local network by name (like archipelago.local) without any DNS server. Devices announce themselves automatically." },
// Cryptography
"blake3": { tech: "Modern cryptographic hash function. Extremely fast (parallelizable, SIMD-optimized), outputs 256-bit hashes. Used by StartOS for S9PK merkle roots and package integrity verification.", plain: "A very fast way to create a unique fingerprint of data. Used to verify that app packages haven't been tampered with." },
"merkle tree": { tech: "Binary tree where each leaf is a hash of a data block and each node is a hash of its children. The root hash verifies the integrity of all data. Enables partial verification without downloading everything.", plain: "A tree-shaped structure of checksums. If you have the top checksum, you can verify any piece of data in the tree without checking everything else." },
"BEP-44": { tech: "BitTorrent Enhancement Proposal 44 \u2014 defines mutable and immutable data storage in the BitTorrent Mainline DHT. Mutable items are signed with Ed25519 and can be updated.", plain: "A way to store and update small pieces of data on the BitTorrent network. Used by did:dht to publish your identity where anyone can find it." },
"Mainline DHT": { tech: "BitTorrent's Distributed Hash Table \u2014 a global peer-to-peer network of millions of nodes storing key-value pairs. No central server. Used by did:dht for decentralized identity publishing.", plain: "A massive worldwide peer-to-peer network (used by BitTorrent). Millions of computers work together to store and look up small pieces of data without any central server." },
"z-base-32": { tech: "Human-friendly base32 encoding variant that avoids visually ambiguous characters (0/O, 1/l). Used to encode Ed25519 public keys in did:dht identifiers.", plain: "A way to write binary data using only letters and numbers, carefully chosen so you can't confuse similar-looking characters." },
"multicodec": { tech: "Self-describing encoding prefix that identifies the type and format of data. E.g., 0xed01 = Ed25519 public key. Used in did:key to make keys self-identifying.", plain: "A tiny label at the start of data that says what kind of data it is. Like a file extension, but built into the data itself." },
"base58btc": { tech: "Base58 encoding as used by Bitcoin. Removes ambiguous characters (0, O, l, I) from base64. Used for did:key identifiers (z-prefixed multibase).", plain: "A way to write binary data using letters and numbers, minus the confusing ones. The same encoding used for Bitcoin addresses." },
"ChaCha20-Poly1305": { tech: "Authenticated encryption cipher (AEAD). ChaCha20 encrypts, Poly1305 authenticates. Fast on CPUs without hardware AES. Used by WireGuard and Noise protocol.", plain: "An encryption method that's both fast and secure, especially on devices without special encryption hardware. Used by VPN connections." },
"ChaCha20-Adiantum": { tech: "Wide-block encryption mode combining ChaCha20 and AES for disk encryption. Fast on CPUs without AES-NI hardware acceleration. Archipelago's LUKS fallback cipher.", plain: "A disk encryption method for computers that don't have special encryption hardware. Just as secure as AES, but works fast on any processor." },
// Frontend Frameworks
"Angular": { tech: "Google's TypeScript-first web framework. Component-based, opinionated (routing, forms, HTTP, i18n built-in). Heavier than React/Vue but more batteries-included. Used by StartOS.", plain: "A comprehensive web framework by Google. Like a fully furnished apartment \u2014 everything is built in. Bigger but you don't need to choose your own tools." },
"React": { tech: "Meta's JavaScript UI library. Component-based, virtual DOM, unidirectional data flow. Lightweight core with ecosystem of add-on libraries. Used by umbrelOS.", plain: "A popular UI toolkit by Meta (Facebook). Flexible and widely used. You pick your own tools for routing, state management, and styling." },
"Vue": { tech: "Progressive JavaScript framework. Template-based, reactive data binding, single-file components. Lighter than Angular, more opinionated than React. Used by Archipelago.", plain: "A web framework that's easy to learn and incrementally adopt. Templates feel natural if you know HTML. Used by Archipelago's dashboard." },
"Vite": { tech: "Modern frontend build tool using native ES modules for instant dev server startup and fast HMR (Hot Module Replacement). Replaces webpack. Used by Archipelago and umbrelOS.", plain: "A build tool that makes web development fast. Instead of bundling everything on each change, it serves files directly and updates instantly." },
"Tailwind": { tech: "Utility-first CSS framework. Classes like 'p-4 text-white bg-blue-500' applied directly in HTML. No custom CSS needed for most styling. Rapid UI development.", plain: "A CSS toolkit where you style elements by adding utility classes directly in the HTML. Like Lego bricks for styling \u2014 compose small pieces into any design." },
"Pinia": { tech: "Official state management library for Vue 3. Type-safe, devtools-integrated, modular stores. Successor to Vuex. Used by Archipelago.", plain: "A data management system for Vue apps. Stores information (like which apps are running) in an organized way that any component can access." },
"Zustand": { tech: "Lightweight React state management library. Simple API, minimal boilerplate, supports middleware and devtools. Used by umbrelOS.", plain: "A simple data store for React apps. Lets different parts of the UI share and react to the same data without complicated setup." },
// Backend
"Express": { tech: "Minimalist Node.js web framework. Handles HTTP routing, middleware, and static file serving. De facto standard for Node.js servers. Used by umbrelOS.", plain: "The most popular web server framework for Node.js. Simple and flexible \u2014 most Node.js web applications are built on it." },
"Axum": { tech: "Rust web framework built on tokio and hyper. Type-safe routing, middleware via tower, async by default. Used by StartOS (startd) for its built-in web server.", plain: "A modern Rust web framework. Fast, safe, and async \u2014 used by StartOS to serve its entire web interface from the Rust backend." },
"Tokio": { tech: "Async runtime for Rust. Provides task scheduling, I/O, timers, and synchronization primitives. The foundation for most async Rust applications.", plain: "The engine that powers async Rust code. Handles thousands of concurrent tasks efficiently \u2014 like a traffic controller for network operations." },
// Auth & Security
"JWT": { tech: "JSON Web Token \u2014 compact, URL-safe token format for transmitting claims between parties. Base64-encoded header.payload.signature. Used by umbrelOS for API authentication.", plain: "A digital pass that proves who you are. The server creates it when you log in, and you show it with every request. The signature prevents tampering." },
"bcrypt": { tech: "Password hashing function with built-in salt and configurable cost factor. Deliberately slow to resist brute-force attacks. umbrelOS uses $2b$ variant with 12 rounds.", plain: "A way to store passwords safely. It's intentionally slow \u2014 fast enough for login, but too slow for an attacker to guess millions of passwords." },
"TOTP": { tech: "Time-based One-Time Password (RFC 6238). Generates 6-digit codes that change every 30 seconds from a shared secret. Used for two-factor authentication (2FA).", plain: "The 6-digit codes from your authenticator app that change every 30 seconds. Even if someone steals your password, they can't log in without the code." },
// Tools
"Kopia": { tech: "Open-source backup tool with encryption, deduplication, and compression. Supports local, cloud, and network storage. Used by umbrelOS for encrypted backups to external drives.", plain: "A backup program that encrypts your data, removes duplicates to save space, and can back up to external drives or cloud storage." },
"isomorphic-git": { tech: "Pure JavaScript implementation of Git. Runs in Node.js and browsers without native git binary. Used by umbrelOS to clone and pull the app store repository.", plain: "Git written entirely in JavaScript. Lets umbrelOS manage its app store repository without needing the regular git program installed." },
"Zod": { tech: "TypeScript-first schema validation library. Defines data shapes and validates at runtime. Used by umbrelOS for validating app manifests and API inputs.", plain: "A tool that checks if data has the right shape and types. Like a bouncer checking IDs \u2014 rejects malformed data before it causes problems." },
// Data & Sync
"binary diffs": { tech: "Efficient encoding of changes between two versions of data. Only the differences are transmitted, not the full state. CBOR-encoded diffs are 2-5x smaller than JSON equivalents.", plain: "Instead of sending all the data every time something changes, only the tiny difference is sent. Like telling someone 'change word 5' instead of resending the whole paragraph." },
"CBOR diffs": { tech: "Differential updates encoded in CBOR (Concise Binary Object Representation). Patch-DB uses these over WebSocket for real-time state sync between backend and frontend.", plain: "Compact binary change-packets that the backend pushes to the browser. Much smaller than JSON \u2014 critical for keeping the UI in sync without wasting bandwidth." },
"COW": { tech: "Copy-on-Write \u2014 filesystem optimization where data is only copied when modified. Original data stays intact. Enables instant, space-efficient snapshots. Core feature of btrfs and ZFS.", plain: "When you modify a file, the filesystem writes the change to a new location instead of overwriting the original. This makes instant backups (snapshots) possible." },
"DAG-CBOR": { tech: "CBOR encoding for directed acyclic graphs. Used by DWN for content-addressed data structures. Messages are encoded as DAG-CBOR with CID (Content Identifier) references.", plain: "A compact binary format for linked data structures. Each piece of data gets a unique address based on its content, making verification automatic." }
}
// Sort by length descending so longer terms match first ("Bitcoin Knots" before "Bitcoin")
const sortedTerms = Object.keys(GLOSSARY).sort((a, b) => b.length - a.length)
// Build a single regex that matches any term (word-boundary aware)
const termPattern = new RegExp(
'\\b(' + sortedTerms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') + ')\\b',
'g'
)
// Walk text nodes and wrap matches
function autoLinkTerms() {
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode(node) {
const parent = node.parentElement
if (!parent) return NodeFilter.FILTER_REJECT
// Skip: already linked, code, script, style, inputs, card names, badges, monospace elements, dep-chain
const tag = parent.tagName.toLowerCase()
if (['script','style','code','pre','input','textarea','button'].includes(tag)) return NodeFilter.FILTER_REJECT
if (parent.closest('.kw, .card-name, .card-image, .dep-chain, .path-tree, .boot-detail, .glossary-term, .proto-card-name, .dep-tag, .card-badge, .pop-title, .popover, code, pre')) return NodeFilter.FILTER_REJECT
if (parent.classList.contains('kw')) return NodeFilter.FILTER_REJECT
return NodeFilter.FILTER_ACCEPT
}
}
)
const textNodes = []
while (walker.nextNode()) textNodes.push(walker.currentNode)
const linked = new Set() // Track which terms we've linked to avoid overloading
let linkCount = 0
for (const node of textNodes) {
const text = node.textContent
if (!termPattern.test(text)) continue
termPattern.lastIndex = 0
const frag = document.createDocumentFragment()
let lastIndex = 0
let match
while ((match = termPattern.exec(text)) !== null) {
const term = match[0]
const key = sortedTerms.find(k => k.toLowerCase() === term.toLowerCase()) || term
// Add text before match
if (match.index > lastIndex) {
frag.appendChild(document.createTextNode(text.slice(lastIndex, match.index)))
}
// Only link first ~3 occurrences of each term to avoid visual overload
const termCount = linked.has(key) ? (linked.get ? 999 : 999) : 0
if (!linked.has(key) || true) { // Link all for now
const span = document.createElement('span')
span.className = 'kw'
span.textContent = term
span.dataset.term = key
span.setAttribute('tabindex', '0')
frag.appendChild(span)
linked.add(key)
linkCount++
} else {
frag.appendChild(document.createTextNode(term))
}
lastIndex = match.index + match[0].length
}
if (lastIndex < text.length) {
frag.appendChild(document.createTextNode(text.slice(lastIndex)))
}
if (frag.childNodes.length > 0) {
node.parentNode.replaceChild(frag, node)
}
}
console.log(`Linked ${linkCount} keyword instances`)
}
// ── Popover show/hide ─────────────────────────────────
let activePopover = null
function showPopover(termEl) {
hidePopover()
const key = termEl.dataset.term
const entry = GLOSSARY[key]
if (!entry) return
// Create overlay to catch outside clicks
const overlay = document.createElement('div')
overlay.className = 'popover-overlay'
overlay.onclick = hidePopover
// Create popover
const pop = document.createElement('div')
pop.className = 'popover'
pop.innerHTML = `
<button class="pop-close" onclick="hidePopover()">&times;</button>
<div class="pop-title">${key}</div>
<div class="pop-section">
<div class="pop-label pop-label-tech">Technical</div>
<div class="pop-text">${entry.tech}</div>
</div>
<div class="pop-section">
<div class="pop-label pop-label-plain">In plain English</div>
<div class="pop-text pop-text-plain">${entry.plain}</div>
</div>
${entry.used ? `<div class="pop-used">${entry.used}</div>` : ''}
`
document.body.appendChild(overlay)
document.body.appendChild(pop)
// Position near the term
const rect = termEl.getBoundingClientRect()
const popRect = pop.getBoundingClientRect()
let top = rect.bottom + 8
let left = rect.left
// Flip above if no room below
if (top + popRect.height > window.innerHeight - 16) {
top = rect.top - popRect.height - 8
}
// Keep within viewport horizontally
if (left + popRect.width > window.innerWidth - 16) {
left = window.innerWidth - popRect.width - 16
}
if (left < 16) left = 16
if (top < 16) top = 16
pop.style.top = top + 'px'
pop.style.left = left + 'px'
activePopover = { overlay, pop }
}
function hidePopover() {
if (activePopover) {
activePopover.overlay.remove()
activePopover.pop.remove()
activePopover = null
}
}
// ── Event delegation for keyword clicks ────────────────
document.body.addEventListener('click', (e) => {
const kw = e.target.closest('.kw')
if (kw) {
e.stopPropagation()
showPopover(kw)
}
})
// Close on Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') hidePopover()
})
// Run auto-linker after DOM is ready
autoLinkTerms()
// ═══════════════════════════════════════════════════════════════
// NORMALIZE DETAIL ROWS — ensure all cards in a tier have the
// same fields in the same order for vertical alignment
// ═══════════════════════════════════════════════════════════════
;(function normalizeDetailRows() {
document.querySelectorAll('.tier-section').forEach(tier => {
const cards = tier.querySelectorAll('.card')
if (cards.length < 2) return
// Collect union of all labels in this tier, preserving first-seen order
const allLabels = []
const labelSet = new Set()
cards.forEach(card => {
card.querySelectorAll('.detail-row .detail-label').forEach(lbl => {
const text = lbl.textContent.trim()
if (!labelSet.has(text)) {
labelSet.add(text)
allLabels.push(text)
}
})
})
// For each card, rebuild detail rows in the canonical order
cards.forEach(card => {
const details = card.querySelector('.card-details')
if (!details) return
// Index existing rows by label
const rowMap = {}
details.querySelectorAll('.detail-row').forEach(row => {
const lbl = row.querySelector('.detail-label')
if (lbl) rowMap[lbl.textContent.trim()] = row
})
// Rebuild in canonical order
const frag = document.createDocumentFragment()
allLabels.forEach(label => {
if (rowMap[label]) {
frag.appendChild(rowMap[label])
} else {
// Create placeholder row so vertical position matches
const row = document.createElement('div')
row.className = 'detail-row'
row.innerHTML = `<span class="detail-label">${label}</span><span class="detail-value" style="color:#3b3f50;">&mdash;</span>`
frag.appendChild(row)
}
})
details.appendChild(frag)
})
})
})()
</script>
</body>
</html>