project-architecture.md
Rama main · réplica de lectura del repositorio original.
# ProxMate — Project Architecture
> A lightweight, invite-only cloud dashboard built on Proxmox VE.
> **Historical document.** This is the original Phase-1 design spec, kept for context. Parts of it
> no longer match the code (for example, sessions are httpOnly cookies, not localStorage, and LXC
> containers are supported). For how ProxMate works today, see the [README](./README.md),
> [DEPLOYMENT.md](./DEPLOYMENT.md), [SECURITY.md](./SECURITY.md) and the [`okf/`](./okf/index.md)
> knowledge bundle.
---
## 1. High-Level Architecture
```mermaid
graph LR
subgraph Client ["Browser"]
A[Next.js SPA]
B[noVNC Client]
end
subgraph ProxMate ["ProxMate Server(s)"]
C[Next.js Dev Server :3000]
D[Express API :4000]
E[(SQLite via Prisma)]
end
subgraph Proxmox ["Proxmox Cluster"]
F[Proxmox REST API :8006]
G[noVNC WebSocket :5900+]
end
A -- "HTTP REST" --> D
B -- "WebSocket" --> D
C -- "Serves UI" --> A
D -- "Prisma Client" --> E
D -- "HTTPS + API Token" --> F
D -- "WSS Proxy" --> G
```
| Layer | Technology | Port | Purpose |
|-------|-----------|------|---------|
| Frontend | Next.js 16 (App Router), TailwindCSS v4, Shadcn/UI (Base UI) | `:3000` | Dashboard UI, VM wizard, noVNC viewer |
| Backend | Node.js + Express 5 + `ws` (WebSocket relay) | `:4000` | REST API, auth, Proxmox proxy, WS relay |
| Database | SQLite 3 via Prisma ORM | file | Users, invites, quotas, VM mappings |
| Proxmox | Proxmox VE REST API | `:8006` | VM lifecycle, ISO listing, console tickets |
---
## 2. Repository Structure
```
ProxMate/
├── frontend/ # Next.js application
│ ├── src/
│ │ ├── app/ # App Router pages
│ │ │ ├── (setup)/ # First-time OOBE wizard
│ │ │ │ └── setup/
│ │ │ │ ├── page.tsx # Step 1: Welcome + admin account
│ │ │ │ ├── proxmox/page.tsx # Step 2: Proxmox connection
│ │ │ │ ├── defaults/page.tsx # Step 3: Storage/network defaults
│ │ │ │ └── complete/page.tsx # Step 4: Review + finish
│ │ │ ├── (auth)/
│ │ │ │ ├── login/page.tsx
│ │ │ │ └── register/[token]/page.tsx
│ │ │ ├── (dashboard)/
│ │ │ │ ├── layout.tsx # Sidebar + topbar shell
│ │ │ │ ├── page.tsx # Dashboard overview
│ │ │ │ ├── vms/
│ │ │ │ │ ├── page.tsx # VM list
│ │ │ │ │ ├── new/page.tsx # Create VM wizard
│ │ │ │ │ └── [id]/
│ │ │ │ │ ├── page.tsx # VM detail
│ │ │ │ │ └── console/page.tsx # noVNC viewer
│ │ │ │ ├── admin/
│ │ │ │ │ ├── invites/page.tsx # Invite management
│ │ │ │ │ ├── users/page.tsx # User management
│ │ │ │ │ └── settings/page.tsx # Re-configure Proxmox connection
│ │ │ │ └── middleware.ts # Redirect to /setup if not configured
│ │ │ ├── layout.tsx # Root layout
│ │ │ └── globals.css
│ │ ├── components/
│ │ │ ├── ui/ # Shadcn/UI primitives
│ │ │ ├── dashboard/ # Dashboard-specific components
│ │ │ ├── vm/ # VM cards, forms, status badges
│ │ │ └── console/ # noVNC wrapper component
│ │ ├── lib/
│ │ │ ├── api.ts # Fetch wrapper for Express API
│ │ │ ├── auth.ts # Auth context / helpers
│ │ │ └── utils.ts # Shared utilities
│ │ └── hooks/
│ │ ├── use-auth.ts
│ │ └── use-vms.ts
│ ├── public/
│ ├── tailwind.config.ts
│ ├── next.config.ts
│ ├── tsconfig.json
│ └── package.json
│
├── backend/ # Express API server
│ ├── src/
│ │ ├── index.ts # Server entry point
│ │ ├── app.ts # Express app setup
│ │ ├── config/
│ │ │ └── env.ts # Environment variable loader
│ │ ├── middleware/
│ │ │ ├── auth.ts # JWT verification middleware
│ │ │ ├── admin.ts # Admin-only guard
│ │ │ ├── cors.ts # CORS configuration
│ │ │ └── errorHandler.ts # Global error handler
│ │ ├── routes/
│ │ │ ├── setup.routes.ts # /api/setup/* (OOBE wizard)
│ │ │ ├── auth.routes.ts # /api/auth/*
│ │ │ ├── invite.routes.ts # /api/invites/*
│ │ │ ├── vm.routes.ts # /api/vms/*
│ │ │ ├── proxmox.routes.ts # /api/proxmox/* (ISOs, nodes)
│ │ │ └── console.routes.ts # /api/vms/:id/console (WS)
│ │ ├── services/
│ │ │ ├── auth.service.ts # Password hashing, JWT signing
│ │ │ ├── invite.service.ts # Token generation & validation
│ │ │ ├── proxmox.service.ts # Proxmox REST API wrapper
│ │ │ ├── vm.service.ts # VM lifecycle orchestration
│ │ │ ├── vnc-proxy.service.ts # WebSocket relay to Proxmox
│ │ │ └── setup.service.ts # First-time config + admin creation
│ │ └── types/
│ │ └── index.ts # Shared TypeScript types
│ ├── prisma/
│ │ ├── schema.prisma # Database schema
│ │ └── migrations/
│ ├── tsconfig.json
│ └── package.json
│
├── .env.example # Template for secrets
├── project-architecture.md # This file
└── README.md
```
---
## 3. Database Schema (Prisma)
```prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL") // file:./proxmate.db
}
model User {
id String @id @default(cuid())
email String @unique
passwordHash String
displayName String
role String @default("user") // "admin" | "user"
// Resource Quotas (set from invite token at registration)
maxCpu Int @default(0) // total vCPU cores allowed
maxRam Int @default(0) // total RAM in MB
maxStorage Int @default(0) // total disk in GB
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
vms VirtualMachine[]
invitesCreated InviteToken[] @relation("CreatedInvites")
usedInvite InviteToken? @relation("UsedInvite")
sessions Session[]
}
model Session {
id String @id @default(cuid())
userId String
token String @unique // JWT jti or refresh token
expiresAt DateTime
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([token])
@@index([userId])
}
model InviteToken {
id String @id @default(cuid())
token String @unique // Crypto-random URL-safe string
createdById String
label String? // Optional admin note
// Quota caps embedded in the invite
maxCpu Int // vCPU cores
maxRam Int // RAM in MB
maxStorage Int // Disk in GB
usedById String? @unique // Null until redeemed
expiresAt DateTime
createdAt DateTime @default(now())
createdBy User @relation("CreatedInvites", fields: [createdById], references: [id])
usedBy User? @relation("UsedInvite", fields: [usedById], references: [id])
@@index([token])
}
model VirtualMachine {
id String @id @default(cuid())
userId String
proxmoxVmId Int // VMID on Proxmox cluster
proxmoxNode String // Target node name
name String
description String?
// Allocated resources
cpu Int // vCPU cores
ram Int // RAM in MB
storage Int // Disk in GB
os String // ISO filename
status String @default("creating") // creating | running | stopped | error
ipAddress String? // Populated after DHCP/cloud-init
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([proxmoxVmId])
}
// Stores application config set during first-time setup (OOBE)
// Key-value store so new settings can be added without migrations
model SystemConfig {
key String @id // e.g. "proxmox_host", "setup_complete"
value String // Stored as string, parsed by app
sensitive Boolean @default(false) // If true, value is AES-encrypted
updatedAt DateTime @updatedAt
}
```
---
## 4. API Routes
### 4.0 First-Time Setup (OOBE)
All `/api/setup/*` routes are **only accessible when `setup_complete` is not set** in `SystemConfig`. Once setup finishes, they return `403`.
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| `GET` | `/api/setup/status` | Public | Returns `{ setupComplete: boolean }` — used by frontend middleware to redirect |
| `POST` | `/api/setup/admin` | Public* | Create the initial admin account (email, password, displayName) |
| `POST` | `/api/setup/proxmox` | Public* | Save Proxmox connection config (host, tokenId, tokenSecret, verifySsl) |
| `POST` | `/api/setup/proxmox/test` | Public* | Test the Proxmox connection and return cluster info (nodes, version) |
| `GET` | `/api/setup/proxmox/resources` | Public* | Fetch available storage pools, network bridges, ISO storages from Proxmox |
| `POST` | `/api/setup/defaults` | Public* | Save default VM settings (storage pool, network bridge, ISO storage) |
| `POST` | `/api/setup/complete` | Public* | Finalize setup — sets `setup_complete=true`, generates JWT_SECRET, returns admin JWT |
> \* These routes are "public" only during first-time setup. The `setupGuard` middleware blocks them entirely once setup is complete.
**Setup wizard steps in the frontend:**
| Step | Page | What the admin sees |
|------|------|---------------------|
| 1 | `/setup` | Welcome message, create admin email + password + display name |
| 2 | `/setup/proxmox` | Enter Proxmox host URL, API Token ID, API Token Secret, SSL toggle — with a "Test Connection" button |
| 3 | `/setup/defaults` | Dropdowns auto-populated from Proxmox: pick default storage pool, network bridge, ISO storage location |
| 4 | `/setup/complete` | Review all settings, confirm, and launch into the dashboard |
### 4.1 Authentication
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| `POST` | `/api/auth/register` | Public | Create account with a valid invite token |
| `POST` | `/api/auth/login` | Public | Email + password → JWT |
| `POST` | `/api/auth/logout` | User | Invalidate session |
| `GET` | `/api/auth/me` | User | Return current user + quota usage |
| `GET` | `/api/auth/invite/:token` | Public | Validate invite token (pre-registration check) |
**Register Request Body:**
```json
{
"email": "user@example.com",
"password": "securePass123",
"displayName": "Jane Doe",
"inviteToken": "abc123xyz"
}
```
**Login Response:**
```json
{
"user": { "id": "...", "email": "...", "role": "user", "displayName": "..." },
"token": "eyJhbGciOi...",
"expiresAt": "2026-06-23T12:00:00Z"
}
```
### 4.2 Admin — Invite Management
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| `POST` | `/api/invites` | Admin | Generate a new invite link |
| `GET` | `/api/invites` | Admin | List all invites with status |
| `DELETE` | `/api/invites/:id` | Admin | Revoke an unused invite |
**Create Invite Request:**
```json
{
"maxCpu": 4,
"maxRam": 8192,
"maxStorage": 100,
"label": "Dev team member",
"expiresIn": "7d"
}
```
**Create Invite Response:**
```json
{
"id": "...",
"inviteUrl": "https://proxmate.local/register/abc123xyz",
"token": "abc123xyz",
"expiresAt": "2026-06-29T23:28:00Z"
}
```
### 4.3 Virtual Machines
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| `GET` | `/api/vms` | User | List user's VMs (admin sees all) |
| `POST` | `/api/vms` | User | Create a new VM |
| `GET` | `/api/vms/:id` | User | VM detail + live Proxmox status |
| `DELETE` | `/api/vms/:id` | User | Destroy VM (Proxmox + DB) |
| `POST` | `/api/vms/:id/start` | User | Start VM |
| `POST` | `/api/vms/:id/stop` | User | Stop VM (graceful shutdown) |
| `POST` | `/api/vms/:id/stop?force=true` | User | Force stop VM |
| `POST` | `/api/vms/:id/restart` | User | Restart VM |
**Create VM Request:**
```json
{
"name": "my-web-server",
"cpu": 2,
"ram": 2048,
"storage": 20,
"os": "ubuntu-24.04-live-server-amd64.iso",
"node": "pve-node-01"
}
```
### 4.4 Proxmox Resources
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| `GET` | `/api/proxmox/nodes` | User | List available Proxmox nodes |
| `GET` | `/api/proxmox/isos` | User | List ISOs available on the cluster |
| `GET` | `/api/proxmox/storage` | User | List storage pools (Ceph, local, etc.) |
### 4.5 Console (WebSocket)
| Protocol | Endpoint | Auth | Description |
|----------|----------|------|-------------|
| `WS` | `/api/vms/:id/console` | User (token query param) | Proxied noVNC WebSocket connection |
---
## 5. First-Time Setup (OOBE) Flow
```mermaid
sequenceDiagram
participant Browser
participant Next as Next.js Middleware
participant API as Express API
participant DB as SQLite
participant PVE as Proxmox API
Browser->>API: GET /api/setup/status
API->>DB: Check SystemConfig for "setup_complete"
API-->>Browser: {setupComplete: false}
Next->>Browser: Redirect to /setup
Note over Browser: Step 1 — Create Admin
Browser->>API: POST /api/setup/admin {email, password, displayName}
API->>DB: Create User (role: admin)
API-->>Browser: {success: true}
Note over Browser: Step 2 — Connect Proxmox
Browser->>API: POST /api/setup/proxmox {host, tokenId, tokenSecret}
API->>DB: Store in SystemConfig (encrypted)
Browser->>API: POST /api/setup/proxmox/test
API->>PVE: GET /api2/json/version
PVE-->>API: {version: "8.x", ...}
API-->>Browser: {connected: true, version: "8.x"}
Note over Browser: Step 3 — Pick Defaults
Browser->>API: GET /api/setup/proxmox/resources
API->>PVE: GET storage, networks, ISOs
API-->>Browser: {storages: [...], bridges: [...], isoStorages: [...]}
Browser->>API: POST /api/setup/defaults {storage, bridge, isoStorage}
API->>DB: Store in SystemConfig
Note over Browser: Step 4 — Finalize
Browser->>API: POST /api/setup/complete
API->>DB: Set "setup_complete" = true
API-->>Browser: {token: "admin-jwt...", redirectTo: "/"}
```
---
## 6. Auth Flow
```mermaid
sequenceDiagram
participant Admin
participant API as Express API
participant DB as SQLite
participant User
Note over Admin: Phase 1 — Invite Creation
Admin->>API: POST /api/invites {maxCpu, maxRam, maxStorage}
API->>DB: Insert InviteToken
API-->>Admin: {inviteUrl, token, expiresAt}
Admin->>User: Share invite URL
Note over User: Phase 2 — Registration
User->>API: GET /api/auth/invite/:token
API->>DB: Validate token (unused, not expired)
API-->>User: {valid: true, quotas: {...}}
User->>API: POST /api/auth/register {email, password, inviteToken}
API->>DB: Create User with quotas from token
API->>DB: Mark InviteToken as used
API-->>User: {user, token}
Note over User: Phase 3 — Normal Login
User->>API: POST /api/auth/login {email, password}
API->>DB: Verify credentials
API->>DB: Create Session
API-->>User: {user, token, expiresAt}
```
---
## 7. VM Creation Flow
```mermaid
sequenceDiagram
participant UI as Next.js Frontend
participant API as Express API
participant DB as SQLite
participant PVE as Proxmox API
UI->>API: POST /api/vms {name, cpu, ram, storage, os, node}
API->>DB: Check user quota (used + requested ≤ max)
alt Quota Exceeded
API-->>UI: 403 {error: "Quota exceeded", details: {...}}
end
API->>DB: Insert VM record (status: "creating")
API->>PVE: POST /api2/json/nodes/{node}/qemu (create VM)
PVE-->>API: {vmid: 105, ...}
API->>DB: Update VM (proxmoxVmId: 105, status: "stopped")
API->>PVE: POST /api2/json/nodes/{node}/qemu/105/status/start
PVE-->>API: {upid: "..."}
API->>DB: Update VM (status: "running")
API-->>UI: {vm: {...}, status: "running"}
```
---
## 8. noVNC Console Proxy Flow
```mermaid
sequenceDiagram
participant Browser as Browser (noVNC)
participant API as Express API
participant PVE as Proxmox API
participant VNC as Proxmox VNC Server
Browser->>API: WS /api/vms/:id/console?token=JWT
API->>API: Verify JWT, check VM ownership
API->>PVE: POST /api2/json/nodes/{node}/qemu/{vmid}/vncproxy
PVE-->>API: {port, ticket, cert}
API->>VNC: Open WebSocket to wss://pve-node:port/?vncticket=...
Note over API: Bidirectional byte relay
Browser<<->>API: noVNC frames
API<<->>VNC: VNC frames
```
**Key implementation details:**
- The Express server uses `express-ws` to handle the incoming WebSocket upgrade from the browser.
- On connection, it requests a VNC ticket from Proxmox's `/vncproxy` endpoint.
- It opens a *second* WebSocket to the Proxmox node using that ticket.
- It then pipes data bidirectionally between the browser and Proxmox.
- The noVNC client on the frontend connects to `ws://proxmate-api:4000/api/vms/:id/console`.
---
## 9. Environment Variables
> **Note:** Proxmox credentials and default VM settings are now configured through the setup wizard and stored in the `SystemConfig` database table (encrypted for sensitive values). The `.env` file only needs server-level config.
```env
# ─── Backend (.env) ──────────────────────────────────────────
# Server
PORT=4000
NODE_ENV=development
FRONTEND_URL=http://localhost:3000
# Database
DATABASE_URL="file:./proxmate.db"
# Encryption key for SystemConfig sensitive values (auto-generated on first setup)
ENCRYPTION_KEY=<auto-generated-on-setup>
# ─── Frontend (.env.local) ───────────────────────────────────
NEXT_PUBLIC_API_URL=http://localhost:4000/api
```
**Values stored in SystemConfig (set via setup wizard):**
| Key | Sensitive | Description |
|-----|-----------|-------------|
| `setup_complete` | No | Whether OOBE has been completed |
| `jwt_secret` | Yes | Auto-generated JWT signing secret |
| `proxmox_host` | No | Proxmox API URL (e.g. `https://192.168.1.100:8006`) |
| `proxmox_token_id` | No | API Token ID (e.g. `proxmate@pam!token`) |
| `proxmox_token_secret` | Yes | API Token Secret |
| `proxmox_verify_ssl` | No | Whether to verify Proxmox TLS certs |
| `default_storage` | No | Default Ceph/storage pool for VM disks |
| `default_bridge` | No | Default network bridge |
| `iso_storage` | No | Storage location for ISOs |
---
## 10. Key Dependencies
### Backend (`backend/package.json`)
| Package | Purpose |
|---------|---------|
| `express` | HTTP framework |
| `express-ws` | WebSocket support on Express |
| `@prisma/client` + `prisma` | ORM + SQLite |
| `bcryptjs` | Password hashing |
| `jsonwebtoken` | JWT auth |
| `cors` | CORS middleware |
| `helmet` | Security headers |
| `zod` | Request validation |
| `axios` | Proxmox API calls |
| `ws` | WebSocket client (for Proxmox relay) |
| `dotenv` | Environment variables |
| `tsx` | TypeScript execution (dev) |
| `typescript` | Type safety |
### Frontend (`frontend/package.json`)
| Package | Purpose |
|---------|---------|
| `next` | React meta-framework |
| `react` / `react-dom` | UI library |
| `tailwindcss` | Utility-first CSS |
| `@shadcn/ui` components | Pre-built accessible components |
| `@novnc/novnc` | VNC client for browser |
| `lucide-react` | Icon library |
| `zustand` | Lightweight state management |
| `axios` | API calls |
| `zod` | Client-side validation |
| `class-variance-authority` | Component variants (Shadcn dep) |
| `clsx` + `tailwind-merge` | Class utilities |
---
## 11. Security Considerations
| Concern | Mitigation |
|---------|-----------|
| **Tenant network isolation** | Per-VM Proxmox firewall blocks RFC1918 (LAN, other VMs, host) while allowing internet — see [SECURITY.md](./SECURITY.md). **Requires the Proxmox cluster firewall to be enabled to take effect.** |
| Cross-tenant access (app layer) | Every VM/console action is authorized via `getOwnedVm`; quotas enforced per user |
| Proxmox credentials in client | Proxmox API token **never** leaves the Express backend; stored AES-256-GCM encrypted |
| VM-create param injection | `name`, `os`, and `node` strictly validated (no comma/path injection into Proxmox config) |
| Invite token brute-force | Tokens are 32-byte crypto-random, single-use (atomic claim), with expiry. App-level rate limiting is **not** built in — add it at the reverse proxy |
| Account enumeration | Login runs bcrypt even for unknown emails (constant-time-ish) |
| JWT theft | 24h JWTs with random `jti`; backed by a `Session` row for server-side revocation. Token is stored in **localStorage** (not httpOnly cookies) — XSS-readable; mitigate via strict CSP and consider a cookie-based session for hardening |
| NoVNC hijacking | WebSocket upgrade requires valid JWT; VM ownership verified before proxy starts; one-time VNC tickets |
| SQL injection | Prisma parameterized queries; Zod input validation |
| XSS | React/Next.js built-in escaping; add a Content-Security-Policy at the proxy (Helmet is enabled but CSP is not configured) |
| CORS | Strict origin whitelist (only `FRONTEND_URL`) |
| Proxmox TLS | `verifySsl` flag in stored config; production should use valid certificates |
---
## 12. Phase Execution Plan
| Phase | Scope | Deliverables |
|-------|-------|-------------|
| **1. Architecture & Scaffolding** | This document + project initialization | Monorepo structure, dependency install, dev scripts |
| **2. Database, Auth & Setup Wizard** | Prisma schema, migrations, OOBE endpoints, auth endpoints | Setup wizard backend, register/login/logout, invite CRUD, JWT middleware |
| **3. Proxmox API Wrapper** | `proxmox.service.ts` + resource routes | List nodes, list ISOs, create/delete/start/stop VMs, status polling |
| **4. Frontend Dashboard & OOBE UI** | Next.js pages + components | Setup wizard UI, login, register, VM list, VM create wizard, admin panel |
| **5. VNC Proxy** | WebSocket relay + noVNC integration | Console page, bidirectional WS proxy, connection lifecycle |
---
## 13. Implementation Addendum (post-build deltas)
All 5 phases are complete and validated against a real Proxmox VE 9.2 cluster. Notable
deltas from the original spec above:
**Endpoints added beyond the original API tables:**
- `GET /api/proxmox/resources` — storages/bridges/ISO-storages (authed; used by VM-create + settings)
- `GET /api/users`, `DELETE /api/users/:id` — admin user management
- `GET /api/admin/settings`, `PUT /api/admin/settings/proxmox`, `POST /api/admin/settings/proxmox/test`, `PUT /api/admin/settings/defaults` — post-setup reconfiguration
- `GET /api/admin/isolation`, `PUT /api/admin/isolation` — tenant network-isolation status/toggle
- `POST /api/vms/:id/console` — issues the one-time VNC ticket consumed by the console WebSocket
**Deviations:**
- **Next.js 16**, not 15 — middleware is now `proxy.ts` and is discouraged for auth, so
setup/auth gating is done with **client-side guards**, not the `middleware.ts` shown in the
repo-structure sketch. UI primitives are shadcn on **Base UI**.
- The console WebSocket relay uses the **`ws`** library on the HTTP `upgrade` event (not
`express-ws`), for Express 5 compatibility.
- JWT is stored in **localStorage** (not httpOnly cookies as section 11 originally implied).
- **Containers (LXC) are not implemented** — ProxMate provisions QEMU VMs only. The isolation
model would extend to LXC if added.
- Default storage dropdown is filtered to **images-capable** pools; per-VM **firewall isolation**
is applied on create (see [SECURITY.md](./SECURITY.md)).
*Originally generated for Phase 1; addendum added after Phase 5 + security review.*