Architecture Decisions
Key architectural decisions for xSwarm dashboard and security infrastructure
Architecture Decisions
This document records significant architectural decisions for the xSwarm platform.
ADR-001: Dashboard Restructure - Project-Centric Architecture
Status: Planned Date: January 2026
Context
The current dashboard has a flat, task-centric view where tasks appear at the global level. This doesn’t match the mental model where tasks belong to projects, and projects are assigned to worker machines.
Decision
Transform the dashboard from flat task-centric view to hierarchical project-centric architecture with worker capacity management and console access.
Key Changes
- Tasks belong to projects (not shown at global level)
- Projects get rich dashboards with activity, chat, console
- Workers show capacity metrics and project assignments
- Project “move” = clone repo + decrypt secrets + npm install + assign worker
Database Schema Updates
Workers Table Additions
disk_total: integer('disk_total'), // MB
disk_free: integer('disk_free'), // MB
ram_total: integer('ram_total'), // MB
cpu_cores: integer('cpu_cores'),
gpu_name: text('gpu_name'),
gpu_vram: integer('gpu_vram'), // MB
tasks_completed: integer('tasks_completed').default(0),
hours_worked: real('hours_worked').default(0),
Projects Table Additions
assigned_worker_id: text('assigned_worker_id').references(() => workers.id, { onDelete: 'set null' }),
color: text('color'), // Unique hex color
last_activity_at: text('last_activity_at'),
New Tables
chat_messages- Global and project-specific chatactivity_log- Project activity timeline
API Endpoints
Workers Routes
POST /workers/:id/heartbeat- Add capacity metricsGET /workers/:id/projects- Get assigned projects
Projects Routes
POST /projects/:id/assign- Assign project to workerGET /projects/:id/activity- Get activity timeline
New Chat Routes
GET /chat?project_id=xxx- Get messagesPOST /chat- Send messageGET /notifications- Cross-project notifications
UI Changes
Main Dashboard
- Remove global tasks section
- Add project cards grid with unique colors, activity indicators
- Add quick stats: total projects, active workers, pending issues
- Add global chat panel (right sidebar) with project switcher
- Add notifications feed from all projects
Per-Project Dashboard
Tabs:
- Overview - Header, activity timeline, current tasks, kanban link
- Tasks - Existing kanban view
- Chat - Project-specific chat panel
- Console - Terminal access to worker (xterm.js)
- Settings - Worker assignment, secrets management
Workers Page
Display per worker:
- Capacity: CPU cores, RAM (used/total), Disk (used/total), GPU
- Assigned Projects: List with colors
- Activity: Tasks completed, hours worked
- Actions: “Move Project” button
Project-Worker Assignment Flow
- Update
projects.assigned_worker_idin DB - Worker detects assignment (polling)
- Worker clones GitHub repo
- Worker fetches + decrypts secrets
- Worker runs install scripts
- Worker reports success/failure
Console Access Architecture (xterm.js)
- Dashboard opens xterm.js terminal
- WebSocket to API → routes to worker
- Worker spawns PTY, bridges to WebSocket
Implementation Order
- Sprint 1: Schema + API (Database, endpoints)
- Sprint 2: Main dashboard + Workers page
- Sprint 3: Per-project dashboard with tabs
- Sprint 4: Assignment flow + Console access
Consequences
- More intuitive navigation aligned with mental model
- Better visibility into worker capacity
- Enables future automatic load balancing
- Requires migration of existing data
ADR-002: Zero-Knowledge User Data Encryption
Status: Planned Date: January 2026
Context
Users store sensitive data in xSwarm: BYOK API keys (OpenAI, Anthropic), project secrets (.env files), planning discussions. The platform should not have access to this data under any circumstances.
Decision
Implement true zero-knowledge encryption where all encryption/decryption happens on the client (CLI or browser). Server only stores encrypted blobs.
Requirements
- Only Requirement: GitHub account (already required for xSwarm)
- No password management: Use device approval model (like Signal/WhatsApp)
- Recovery: Single recovery code for disaster scenarios
Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ ADMIN DATABASE (xSwarm-readable) │
│ - User auth (GitHub ID, email) │
│ - Billing (Stripe ID, subscription) │
│ - Project metadata (IDs, task counts for billing) │
│ - Device registry (public keys for key transfer) │
│ - Pending approvals (encrypted key blobs in transit) │
└─────────────────────────────────────────────────────────────┘
│
│ User DB URL (server can connect
│ but only sees encrypted blobs)
▼
┌─────────────────────────────────────────────────────────────┐
│ USER PERSONAL DATABASE (encrypted) │
│ - Projects (name, description, repo_url encrypted) │
│ - Tasks (title, description, criteria encrypted) │
│ - BYOK API keys (OpenAI/Anthropic keys encrypted) │
│ - Project secrets (.env files encrypted) │
│ - Planning meetings (specs, decisions encrypted) │
└─────────────────────────────────────────────────────────────┘
Key Management: Device Approval Model
First Device Ever
- GitHub OAuth login
- System generates master encryption key
- System generates recovery code: “ALPHA-BRAVO-CHARLIE-DELTA”
- Display: “Save this recovery code somewhere safe”
- User confirms (checkbox)
- Key stored locally (~/.xswarm/key or IndexedDB)
New Device (Have Existing Device)
- GitHub OAuth login on new device
- New device generates keypair, registers public key
- “Waiting for approval from existing device…”
- Existing device shows: “New device requesting access [Approve] [Deny]”
- User clicks Approve
- Existing device encrypts master key with new device’s public key
- Encrypted blob sent via server (server can’t decrypt)
- New device decrypts, stores key locally
Disaster Recovery (Lost All Devices)
- GitHub OAuth login
- “No approved devices found. Enter recovery code:”
- User enters recovery code
- Master key derived from recovery code (PBKDF2)
- Key stored locally, device registered as approved
Browser ↔ CLI (Same Machine)
- CLI runs localhost HTTP server on port 19284
- Browser detects localhost server
- Browser requests key transfer
- CLI prompts user: “Browser requesting access [Allow]”
- User allows → key sent over localhost (never hits internet)
Encryption Details
Algorithm: AES-256-GCM (WebCrypto API - native in browser + Node.js) Encrypted data format:
{
"iv": "<base64-96-bit-random>",
"ciphertext": "<base64-encrypted-data>",
"version": 1
}
Database Schema Changes
Admin DB Additions
// Users table additions
user_db_url: text('user_db_url'), // Turso URL for user's encrypted DB
user_db_token: text('user_db_token'), // Auth token for user DB
recovery_code_hash: text('recovery_code_hash'), // bcrypt hash (to verify, not derive)
encryption_version: integer('encryption_version').default(1),
// New table: devices (for device approval flow)
devices: {
id, user_id,
name: text, // "Chad's MacBook Pro"
device_type: text, // "cli" | "web"
public_key: text, // For end-to-end key transfer
created_at, last_seen_at,
is_approved: boolean
}
// New table: pending_approvals (temporary, during key transfer)
pending_approvals: {
id, user_id,
requesting_device_id,
encrypted_key_blob: text, // Encrypted with requester's public key
created_at, expires_at // Auto-expire after 10 minutes
}
// New table: projects_metadata (for billing - no sensitive data)
projects_metadata: {
id, user_id, slug, status,
task_count, last_activity_at,
kanban_token, kanban_enabled
}
User DB Schema
// All *_encrypted fields store JSON: {iv, ciphertext, version}
projects: { id, slug, name_encrypted, description_encrypted, ... }
tasks: { id, project_id, status, title_encrypted, description_encrypted, ... }
byok_api_keys: { id, provider, key_encrypted, name_encrypted, ... }
project_secrets: { id, project_id, file_path, content_encrypted }
planning_meetings: { id, project_id, status, title_encrypted, ... }
Implementation Files
New Files
| File | Purpose |
|---|---|
packages/shared/crypto.js | Isomorphic encrypt/decrypt, keypair generation |
packages/shared/user-db-schema.js | User DB Drizzle schema |
packages/shared/wordlist.js | Recovery code word list (BIP39 subset) |
packages/api/src/db/schema.js | devices, pending_approvals tables |
packages/api/src/services/user-db.js | Per-user DB provisioning via Turso API |
packages/api/src/routes/devices.js | Device registration, approval endpoints |
packages/web/src/stores/crypto.js | Svelte store for encryption key |
packages/web/src/components/DeviceApproval.svelte | Approval UI |
packages/app/src/daemon/crypto.js | CLI encryption + localhost server |
Modified Files
| File | Changes |
|---|---|
packages/api/src/db/schema.js | Add devices, pending_approvals, projects_metadata |
packages/api/src/routes/auth.js | Device registration on login |
packages/app/src/daemon/auth.js | Store/load encryption key, run localhost server |
packages/app/src/index.js | Handle first-time setup flow |
packages/web/src/stores/auth.js | Integrate device approval flow |
Security Considerations
- Master key never sent to server - only encrypted blobs transit
- Device keypairs for transfer - asymmetric encryption prevents server access
- Recovery code → key derivation - PBKDF2 with 310k iterations
- Unique IV per encryption - prevents pattern analysis
- Authenticated encryption (GCM) - detects tampering
- Pending approvals expire - 10 minute window limits exposure
- Localhost-only browser transfer - never hits internet
- No recovery without code - true zero-knowledge trade-off
Implementation Phases
- Phase 1: Crypto Module - AES-256-GCM, keypair generation, recovery codes
- Phase 2: Database Schema - devices, pending_approvals, user DB schema
- Phase 3: Device API - registration, approval, key transfer endpoints
- Phase 4: CLI Integration - setup flow, localhost server, encryption wrapper
- Phase 5: Web Integration - device approval UI, IndexedDB storage
- Phase 6: Testing - unit tests, integration tests, security audit
Consequences
- True zero-knowledge: xSwarm cannot access user data
- Frictionless day-to-day: no passwords, just device approval
- Disaster recovery: single code to save
- Complexity: multi-device key synchronization
- Migration: existing users need onboarding flow
Verification Checklists
Dashboard Restructure
- Build passes:
npm run build - Project cards render with colors
- Workers show capacity metrics
- Project assignment works
- Console tab opens terminal
Zero-Knowledge Encryption
- First device setup shows recovery code
- Recovery code can restore access (all devices lost)
- New device can be approved from existing device
- Key transfer is end-to-end encrypted (verify server logs)
- Data in User DB is encrypted (verify via direct DB query)
- Wrong recovery code fails gracefully
- CLI and web can share key via localhost
- Revoking a device removes its access
- CLI and web use same encryption (interoperable)