Mandatory rules for all new code based on 33 pentest findings. Covers: input validation, auth checks, SSRF prevention, session management, CSP, nginx config, container security, RBAC. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
17 KiB
CLAUDE.md — Archipelago (Archy) Project Guide
Project Overview
Archipelago is a Bitcoin Node OS — a bootable, self-sovereign personal server you flash to USB, install on hardware, and manage via a web UI. Similar to Umbrel/Start9/RaspiBlitz but custom-built with production-grade security.
Stack: Rust backend + Vue 3 (Composition API) + TypeScript (strict) + Vite 7 + Tailwind CSS + Pinia + Podman Target OS: Debian 12 (Bookworm) — x86_64 and ARM64 Current version: 0.1.0
BETA FREEZE — ACTIVE (2026-03-18)
Goal: Ship a flawless beta that works perfectly on every machine we install it on.
We are in beta stabilization mode. The current feature set is LOCKED. Every session must push toward this goal.
Pipeline
PHASE 1: Feature Testing (internal) ← WE ARE HERE
↓ Gate: every feature works, bugs fixed, security hardened, ISO verified
PHASE 2: User Testing (real users on real hardware we don't control)
↓ Gate: user-reported issues resolved, telemetry shows stable fleet
PHASE 3: Beta Live (public release)
What IS allowed
- Bug fixes for existing features
- Security hardening and testing
- Beta telemetry / node reporting (TASK-12 — needed for user testing)
- UI/layout rearrangements (moving things around, improving flow)
- Boot screen completion (FEATURE-4 — already in progress)
- Testing all features end-to-end on fresh installs
- Performance and reliability improvements to existing code
- ISO build hardening
What is NOT allowed
- New features (watch-only wallet, mesh balance check, etc. are POST-BETA)
- New app integrations
- New backend modules or RPC endpoints (unless fixing existing bugs or beta telemetry)
- New dependencies (unless required for beta infrastructure)
- Scope creep of any kind
Status tracking
- Progress tracker:
docs/BETA-PROGRESS.md— updated every session - Beta checklist:
docs/BETA-RELEASE-CHECKLIST.md— the acceptance criteria - Master plan:
docs/MASTER_PLAN.md— phased roadmap (Phase 1/2/3)
Session protocol
- Read
docs/BETA-PROGRESS.mdat start of every session - Report current phase and status before starting work
- Work only on current-phase items
- Update
docs/BETA-PROGRESS.mdat end of every session with what changed
Quick Reference
# Frontend local dev (mock backend on :5959, Vite on :8100)
cd neode-ui && npm start
# Deploy to live server (frontend + backend + restart services)
./scripts/deploy-to-target.sh --live
# Deploy to both servers
./scripts/deploy-to-target.sh --both
# Frontend build (outputs to web/dist/neode-ui/)
cd neode-ui && npm run build
# Type-check frontend
cd neode-ui && npm run type-check
# Rust checks (run on dev server, NOT macOS)
cargo clippy --all-targets --all-features
cargo fmt --all
cargo test --all-features
Dev server: http://192.168.1.228 | Local frontend: http://localhost:8100 (password: password123)
Architecture
Debian 12 (Bookworm)
├── Podman (rootless containers)
├── Nginx (port 80 → proxies /rpc/, /ws/, /health to backend)
├── Rust Backend (core/) — binary on port 5678
│ ├── core/archipelago/ — Main binary, RPC endpoints
│ ├── core/container/ — PodmanClient, manifest parser, dependency resolver, health monitor
│ ├── core/security/ — AppArmor profiles, secrets manager, Cosign image verifier
│ ├── core/performance/ — Resource manager
│ └── core/parmanode/ — Parmanode compatibility layer
└── Vue.js UI (neode-ui/)
├── src/api/ — RPC client (rpc-client.ts), WebSocket, container client
├── src/stores/ — Pinia stores
├── src/views/ — Page components
├── src/components/ — Reusable components
├── src/router/ — Vue Router
├── src/types/ — TypeScript type definitions
└── src/style.css — Global styles + Tailwind utilities
Data Paths (Server)
- App data:
/var/lib/archipelago/{app-id}/ - Secrets:
/var/lib/archipelago/secrets/{app-id}/(encrypted) - Frontend:
/opt/archipelago/web-ui/ - Backend binary:
/usr/local/bin/archipelago - Systemd service:
/etc/systemd/system/archipelago.service - Nginx config:
/etc/nginx/sites-available/archipelago
CRITICAL Workflow Rules
1. NEVER Build Rust on macOS for Linux
Always rsync source to the Linux dev server and build there. Building on macOS and copying the binary causes Exec format errors.
# Deploy does this automatically:
./scripts/deploy-to-target.sh --live
2. Always Deploy After Changes
After editing code (frontend, backend, scripts, or configs), deploy to the live server. Do not leave deployment to the user.
3. Frontend Build Output Path
Frontend builds to web/dist/neode-ui/ — NOT neode-ui/dist/.
4. Deploy-Test-Fix Loop
- Make the change
- Deploy with
./scripts/deploy-to-target.sh --live - Test at http://192.168.1.228
- If broken, fix and redeploy — repeat until working
- End loop only when everything works
5. SSH Access
- Primary:
archipelago@192.168.1.228— password:EwPDR8q45l0Upx@ - Secondary:
archipelago@192.168.1.198 - Credentials stored in gitignored
scripts/deploy-config.sh
sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228
Frontend Rules (Vue.js + TypeScript)
Component Standards
- Always
<script setup lang="ts">— never Options API, never plain JS - Pinia for all state management — focused single-purpose stores
- TypeScript strict mode — no
any, useunknownor proper types - Export types from dedicated
.types.tsfiles - Use type guards for runtime type checking
Styling — Global Classes Only
- ALWAYS create global utility classes in
neode-ui/src/style.css - NEVER use inline Tailwind classes directly in components
- Use semantic class names:
.glass-card,.glass-button,.gradient-button,.path-option-card
API Client Rules
- Use
@/api/rpc-client.tsfor RPC calls,@/api/container-client.tsfor containers - NEVER hardcode API endpoints — use environment variables
- Handle loading states, error states, retry logic for all async operations
CSS Class Hierarchy
| Class | Use | Hover |
|---|---|---|
.path-option-card |
Section containers, interactive cards (Settings-style) | Lifts -2px |
.glass-card |
Content containers, modals, panels | No |
.info-card |
Status badges, metric displays | No |
.info-card-button |
Action buttons inside info sections | Lifts, brightens |
bg-black/20 rounded-xl border border-white/10 |
Info sub-cards inside sections | No |
bg-white/5 |
Simple read-only info rows | No |
.glass-button |
ALL buttons (primary and secondary) | Subtle brighten |
.path-action-button |
Large action buttons (Logout, Continue) | Lifts -2px |
BANNED Classes — Do NOT Use
.gradient-button— REMOVED. Use.glass-buttoninstead. The gradient style breaks the clean glass aesthetic..gradient-card/.gradient-card-dark— REMOVED. Use.glass-cardor.path-option-cardinstead.
Design Tokens
- Font: Avenir Next (primary), Montserrat (
font-archipelago) - Spacing: 4px grid system, 16px default padding
- Glassmorphism:
background: rgba(0,0,0,0.60),backdrop-filter: blur(24px),inset 0 1px 0 rgba(255,255,255,0.22) - Transitions:
all 0.3s easestandard,translateY(-2px)hover,translateY(1px)active - Accent orange (Bitcoin):
#fb923c—#f59e0b - Green (success):
#4ade80| Red (danger):#ef4444| Blue (info):#3b82f6 - Text:
rgba(255,255,255,0.9)primary,rgba(255,255,255,0.6-0.7)muted
Tailwind Custom Values
- Blur:
backdrop-blur-glass(18px),backdrop-blur-glass-strong(24px) - Colors:
glass-dark(0,0,0,0.35),glass-darker(0,0,0,0.6),glass-border(255,255,255,0.18) - Shadows:
shadow-glass,shadow-glass-inset
Backend Rules (Rust)
Error Handling
- No
unwrap()orexpect()in production code — use?operator thiserrorfor library error types,anyhowfor application errors- Custom error types per module:
{module}::Error - Include context:
.context("What failed and why")
RPC Endpoints
- Use
rpc_toolkit::commandmacro for all endpoints - Use
#[context] ctx: RpcContextfor context - Return
Result<T, Error>— validate all inputs before processing
Async & Runtime
tokioruntime only — never mix with other async runtimes- Set timeouts on all external operations
- Use
select!for racing futures with timeouts - Handle shutdown gracefully with cancellation tokens
Code Organization
- New modules in
core/{module-name}/, add tocore/Cargo.tomlmembers snake_casefor all modules/files- Run
cargo clippy --all-targets --all-featuresandcargo fmt --allbefore commits
Logging
- Use
tracingfor structured logging — neverprintln! - Never log secrets, passwords, keys, or tokens
- Include context:
tracing::info!(user_id = %id, "Action")
Container & Security
App Manifests
- All manifests in
apps/{app-id}/manifest.yml - Follow spec in
docs/app-manifest-spec.md - Use
archipelago_container::PodmanClient— NEVER call Docker directly
Security Requirements (Non-Negotiable)
- ALWAYS
readonly_root: trueunless explicitly needed - ALWAYS drop all capabilities, add only required ones
- ALWAYS run as non-root user (UID > 1000)
- ALWAYS
no-new-privileges: true - NEVER use
latesttag — pin specific image versions - NEVER hardcode secrets — use
core/security/secrets_manager.rs
App Icons
Single source of truth: neode-ui/public/assets/img/app-icons/
Naming: {app-id}.{png|webp|svg} — do not duplicate elsewhere.
Security Standards (Post-Pentest — Mandatory)
These rules come from a full penetration test (33 findings, all remediated). Follow them for ALL new code.
Backend (Rust)
- Backend binds to 127.0.0.1 ONLY — never
0.0.0.0. All external access goes through nginx. - Validate ALL user input before path construction — reject
..,/,\, null bytes. Use the existingvalidate_app_id()pattern intor.rs. - Never pass user input to
sudocommands — if unavoidable, validate strictly against an allowlist of characters[a-zA-Z0-9_-]. - Every HTTP endpoint that returns sensitive data MUST check authentication — use
self.is_authenticated(&headers).awaitor be inUNAUTHENTICATED_METHODSwith justification. - Rate-limit authentication endpoints —
extract_client_ip()must only trustX-Real-IPfrom the loopback interface (127.0.0.1). - Federation messages require ed25519 signatures — never accept unsigned peer-joined messages.
- RBAC: use explicit allowlists, not prefix matching —
method.starts_with("node.")is BANNED. List exact methods per role. - Session cookies:
SameSite=Lax; HttpOnly; Path=/—Strictbreaks iframe app fetches.Laxstill prevents CSRF on POST. - Destructive operations require password re-verification — factory reset, onboarding reset, identity export.
- Remember-me secrets: use
OsRngrandom bytes — never derive from/etc/machine-idor other public data. - Rotate session tokens after privilege escalation — TOTP verification must issue a new token, invalidating the pending one.
- Tar archive extraction: validate every entry path — never use
archive.unpack(). Iterate entries and verify no..components or paths escaping the target directory.
Frontend (Vue/TypeScript)
- Validate redirect URLs — use
isLocalRedirect()fromrouter/index.tsbefore anywindow.location.hrefassignment. Rejectjavascript:, protocol-relative (//), and external URLs. - Never use
v-htmlwith user input — if unavoidable, always sanitize withDOMPurify.sanitize(). - CSP: no
unsafe-inlineinscript-src— Vite builds don't need it. Keepunsafe-inlineonly instyle-srcfor Tailwind.
Nginx
- Session validation:
$cookie_session(not$cookie_session_id) — cookie name must match the Rust backend'ssession=cookie. - Prefer
auth_requestover cookie-presence checks —if ($cookie_session = "")only checks presence, not validity. For sensitive endpoints, use nginxauth_requestto validate against the backend. - All
/app/*proxies are unauthenticated at nginx level — each app must handle its own auth. Never expose apps with default credentials (change Grafanaadmin/adminon first boot, etc.).
SSRF Prevention
- Validate all user-supplied URLs — require
https://scheme, reject private IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, ::1, fc00::/7). - Disable redirect following — use
redirect(Policy::none())on reqwest clients that fetch user-supplied URLs. - Onion addresses: validate v3 format — exactly 56 base32
[a-z2-7]chars +.onion. - Webhook URLs: parse with
Url::parse— don't split on:for host extraction (breaks IPv6).
Container Security
- Memory limits on every container — use
--memory=$(mem_limit <name>)pattern fromfirst-boot-containers.sh. Prevents one container from OOM-killing the system. - Health checks on every container — define via
--health-cmdinpodman run. - User-stopped tracking — when a user stops a container via UI, record in
user-stopped.jsonso crash recovery and health monitor don't auto-restart it.
Code Quality
- Zero compiler warnings (Rust and TypeScript)
- Zero linter errors (clippy, eslint)
- Functions under 50 lines, single responsibility
- Comment WHY not WHAT — code should be self-documenting
- Remove dead code entirely — never comment it out
- No
TODO/FIXMEin commits — fix now or create issues - Workspace-relative paths only — NEVER hardcode
/Users/dorian/...
Git Conventions
Commit Format
type: description
Types: feat:, fix:, docs:, refactor:, test:, chore:, perf:
Rules
- Atomic commits — one logical change per commit
mainbranch always production-ready- Feature branches:
feature/description, bug fixes:fix/description - Never commit secrets,
.envfiles, or credentials - Tag releases:
v1.2.3(SemVer)
App Integration Checklist
When adding or fixing apps, every file below must be checked. Missing any one causes failures on fresh installs.
Backend (Rust)
core/archipelago/src/api/rpc/package.rs—get_app_config(): ports, volumes, env vars, custom argscore/archipelago/src/api/rpc/package.rs—needs_archy_net: add if app needs container DNScore/archipelago/src/api/rpc/package.rs—get_app_capabilities(): add required caps (CHOWN, etc.)core/archipelago/src/api/rpc/package.rs— dependency checks (e.g., electrs requires bitcoin)core/archipelago/src/container/docker_packages.rs—get_app_metadata(): title, description, icon, repocore/archipelago/src/container/docker_packages.rs— UI address mapping (e.g.,http://localhost:50002)
Frontend (Vue)
neode-ui/src/views/Marketplace.vue—getCuratedAppList(): marketplace entry with dockerImageneode-ui/src/stores/appLauncher.ts— port-to-proxy mapping (if app has custom UI port)neode-ui/src/views/AppDetails.vue— route ID mapping (if app ID differs from container name)
Nginx
image-recipe/configs/nginx-archipelago.conf—/app/{id}/proxy in HTTP blockimage-recipe/configs/snippets/archipelago-https-app-proxies.conf—/app/{id}/proxy in HTTPS block- Any custom status endpoints (e.g.,
/electrs-status) proxied before the SPA catch-all
Deploy & First Boot
scripts/deploy-to-target.sh— container creation/update logicscripts/first-boot-containers.sh— container created on fresh ISO install- Custom UI containers (e.g., electrs-ui): built and started in both deploy and first-boot
ISO Build
image-recipe/build-auto-installer-iso.sh—CAPTURE_PATTERNS: image captured from live serverimage-recipe/build-auto-installer-iso.sh—CONTAINER_IMAGES: fallback image pulled from registryimage-recipe/build-auto-installer-iso.sh— docker UI source files bundled for build fallbackimage-recipe/build-auto-installer-iso.sh— installer copies files to target disk
Runtime Verification
- Test the app UI loads on its configured port
- Auto-connect dependencies (Bitcoin RPC, LND, etc.) — apps must work out of the box
- Most apps launch in iframe; BTCPay (23000) and Home Assistant (8123) open in new tab (X-Frame-Options)
ISO Build
Build on the target server (has all dependencies):
ssh archipelago@192.168.1.228
cd ~/archy/image-recipe
sudo ./build-auto-installer-iso.sh
# Result: results/archipelago-auto-installer-*.iso
After testing on live server, always update ISO build to include changes. Sync system configs:
archipelago.service→image-recipe/configs/nginx-archipelago.conf→image-recipe/configs/
Key Documentation
docs/architecture.md— System architecturedocs/current-state.md— Current development phasedocs/development-setup.md— Local dev setupdocs/app-manifest-spec.md— YAML manifest specBUILD-GUIDE.md— ISO build guideDEPLOYMENT.md— Deployment detailsCHANGELOG.md— Version history