# 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= # ─── 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.*