Multi-tenant control plane for managing LXC containers on Proxmox VE. Provides a REST API and admin dashboard for registering compute nodes, defining project templates, and provisioning isolated tenant instances with automatic networking, DNS routing, and billing.
Built with Go, PostgreSQL, and zero external ORMs.
+-----------------+
| Admin UI |
| (WebAuthn + |
| HTMX) |
+--------+--------+
|
+---------------+ REST API +---------v---------+ +------------+
| SvelteKit | -------------> | | --> | PostgreSQL |
| Frontend | JWT / Bearer | Controlplane | | 17 |
+---------------+ | (Go) | +------------+
| |
+--+-----+------+----+
| | |
+--------+ +--+--+ ++--------+
| | | |
+------v---+ +----v--+ +-v--------+ +-v--------+
| Proxmox | | Caddy | | WireGuard| | Stripe |
| VE API | | Admin | | wg0 | | Billing |
+----------+ +-------+ +----------+ +----------+
|
+------v--------+
| LXC Containers |
| (tenants) |
+-----------------+
- Multi-tenant provisioning -- async LXC container lifecycle with bounded concurrency (10 parallel jobs) and state machine (provisioning -> active/error -> deleting -> deleted)
- Proxmox VE integration -- custom API client for container clone, start, stop, delete, node status, and task polling
- Admin dashboard -- WebAuthn/passkey authentication, HTMX-powered UI for managing nodes, projects, tenants, audit logs, and settings
- User self-service -- JWT auth with refresh tokens, user-facing tenant CRUD with automatic node selection and SSO token generation
- Dynamic DNS routing -- Caddy Admin API integration for automatic
{subdomain}.{domain}reverse proxy routes with startup reconciliation - WireGuard mesh -- optional peer management with key generation, QR codes, config export, and live
wg0synchronization - Stripe billing -- subscription management with checkout sessions, customer portal, webhook processing, and tier enforcement
- Audit logging -- fire-and-forget database inserts for all CRUD operations
- Security hardened -- CSRF protection, CSP strict, HSTS, rate limiting, constant-time token comparison, AES-256-GCM encryption, 1MB body limit, Slowloris protection
| Component | Technology |
|---|---|
| Language | Go 1.24 |
| HTTP Router | chi/v5 |
| Database | PostgreSQL 17 (pgx, raw SQL, no ORM) |
| Migrations | golang-migrate (embedded SQL) |
| Auth (Admin) | WebAuthn/Passkeys |
| Auth (Users) | JWT (HMAC-SHA256) with refresh tokens |
| Auth (API) | Bearer token (constant-time compare) |
| Encryption | AES-256-GCM |
| Rate Limiting | httprate (IP-based) |
| CSRF | nosurf |
| Billing | Stripe |
| VPN | WireGuard (wgctrl) |
| Logging | log/slog (structured JSON) |
| Container | Docker multi-stage (Alpine 3.21) |
# Clone the repo
git clone https://github.com/yourusername/controlplane.git
cd controlplane
# Configure environment
cp .env.example .env
# Edit .env with your values (see Configuration below)
# Start services
docker compose up -d
# The API starts on :8080 with automatic database migrations.
# Set SETUP_TOKEN in .env for initial WebAuthn passkey registration.All configuration is via environment variables. See .env.example for the full list.
| Variable | Description |
|---|---|
API_TOKEN |
Bearer token for API authentication |
ENCRYPTION_KEY |
64-char hex key for AES-256-GCM encryption |
JWT_SECRET |
64-char hex key for HMAC-SHA256 JWT signing |
POSTGRES_PASSWORD |
PostgreSQL password |
| Variable | Default | Description |
|---|---|---|
BIND_ADDR |
127.0.0.1 |
Listen address |
HOST_PORT |
8080 |
Listen port |
SETUP_TOKEN |
-- | Required for first WebAuthn registration |
REGISTRATION_TOKEN |
-- | If set, user registration requires this token |
CORS_ORIGINS |
localhost:5173,5174 |
Comma-separated allowed origins |
WG_HUB_PUBLIC_KEY |
-- | Enables WireGuard module |
CADDY_ADMIN_URL |
-- | Enables dynamic DNS routing |
STRIPE_SECRET_KEY |
-- | Enables Stripe billing |
GET /healthz Health check (DB ping)
POST /api/v1/auth/register Register (requires REGISTRATION_TOKEN if set)
POST /api/v1/auth/login Login (returns JWT + refresh token)
POST /api/v1/auth/refresh Refresh JWT
GET /api/v1/auth/me Current user profile
POST /api/v1/auth/logout Revoke refresh token
POST /api/v1/auth/password Change password
GET /api/v1/user/tenants List own tenants
POST /api/v1/user/tenants Create tenant (auto-selects node + project)
GET /api/v1/user/tenants/{id} Get tenant
DELETE /api/v1/user/tenants/{id} Delete tenant
POST /api/v1/user/tenants/{id}/suspend Suspend tenant
POST /api/v1/user/tenants/{id}/resume Resume tenant
POST /api/v1/user/tenants/{id}/sso-token Generate SSO token for dashboard access
GET /api/v1/nodes List compute nodes
POST /api/v1/nodes Register a Proxmox node
GET /api/v1/nodes/{id} Get node details
PUT /api/v1/nodes/{id} Update node
DELETE /api/v1/nodes/{id} Remove node
GET /api/v1/projects List project templates
POST /api/v1/projects Create project template
GET /api/v1/projects/{id} Get project
PUT /api/v1/projects/{id} Update project
DELETE /api/v1/projects/{id} Delete project
GET /api/v1/tenants List all tenants
POST /api/v1/tenants Provision tenant (async, returns 202)
GET /api/v1/tenants/{id} Get tenant
PUT /api/v1/tenants/{id} Update tenant
DELETE /api/v1/tenants/{id} Deprovision tenant
POST /api/v1/tenants/{id}/suspend Suspend tenant (stop LXC)
POST /api/v1/tenants/{id}/resume Resume tenant (start LXC)
GET /api/v1/wireguard/peers List peers
POST /api/v1/wireguard/peers Create peer
GET /api/v1/wireguard/peers/{id} Get peer
PUT /api/v1/wireguard/peers/{id} Update peer
DELETE /api/v1/wireguard/peers/{id} Delete peer
POST /api/v1/wireguard/peers/{id}/enable Enable peer
POST /api/v1/wireguard/peers/{id}/disable Disable peer
GET /api/v1/wireguard/peers/{id}/config Download WireGuard config
GET /api/v1/wireguard/peers/{id}/qr QR code for mobile setup
POST /api/v1/billing/checkout Create Stripe checkout session
POST /api/v1/billing/portal Create Stripe customer portal
GET /api/v1/billing/status Get billing status
POST /api/v1/stripe/webhook Stripe webhook (signature verified)
GET /admin/ Dashboard overview
/admin/nodes Node management
/admin/projects Project templates
/admin/tenants Tenant management
/admin/audit Audit log viewer
/admin/settings System settings
/admin/network WireGuard peers (if enabled)
controlplane/
cmd/server/main.go Entry point, config, DB, graceful shutdown
internal/
config/ Environment-based configuration
database/ PostgreSQL pool + embedded SQL migrations (16 files)
server/ chi router, middleware, security headers
response/ JSON helpers, pagination
node/ Compute node CRUD + Proxmox client cache
project/ Project template CRUD
tenant/ Tenant CRUD, user handler, state machine
provisioner/ Async LXC provisioning (bounded concurrency)
proxmox/ Proxmox VE API client (LXC lifecycle, tasks)
sshexec/ SSH exec for in-container operations
station/ Streaming station management + status poller
crypto/ AES-256-GCM encryption
health/ Health check with DB ping
admin/ WebAuthn dashboard (HTMX + Alpine.js)
audit/ Fire-and-forget audit logging
auth/ JWT auth, middleware, token store
user/ User accounts
wireguard/ WireGuard peer management
billing/ Stripe integration
caddy/ Dynamic reverse proxy routing
docker-compose.yml PostgreSQL + controlplane
Dockerfile Multi-stage build (Go 1.24 -> Alpine 3.21)
- No ORM -- raw SQL via pgx for full control over queries and migrations
- Async provisioning -- tenant creation returns 202 immediately; a background worker handles the Proxmox API calls with a 10-slot semaphore
- State machine at SQL level --
WHERE status = $expectedfor compare-and-swap safety, no TOCTOU races - Atomic RAM reservation --
allocated_ram_mb + N <= total_ram_mbenforced in SQL - Graceful shutdown -- SIGTERM triggers provisioner drain (10s timeout), then HTTP shutdown
- Modular integrations -- WireGuard, Caddy, and Stripe are opt-in via environment variables; the system runs without them
go test ./...MIT