Fleet management control plane for Telemt MTProto proxy nodes
Quick Install • Features • Architecture • Configuration • Development • Docker
| Feature | Description | |
|---|---|---|
| 📊 | Fleet Dashboard | Real-time monitoring with metrics, health indicators, and alerts |
| 👥 | Managed Clients | Centralized client management with secret rotation and quotas |
| 🤖 | Agent System | Lightweight per-node agents with mTLS enrollment and gRPC streaming |
| 🗄️ | Dual Storage | SQLite for dev/lightweight, PostgreSQL for production |
| 🔄 | Self-Update | Panel and agents update themselves from GitHub Releases |
| 📦 | Embedded UI | Single binary ships the React dashboard — no separate web server |
| 🔐 | TOTP 2FA | Optional two-factor authentication for operator accounts |
| 🛡️ | RBAC | Viewer, Operator, and Admin roles with middleware enforcement |
sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/lost-coder/panvex/main/deploy/install.sh)"Interactive wizard: ports, storage, TLS, firewall, admin account — all configured step by step.
The control-plane embeds the installer and serves it at
<panel>/install-agent.sh, so once you have a running panel:
sudo bash -c "$(curl -fsSL https://panel.example.com/install-agent.sh)"For the GitHub-hosted bootstrap script (when no panel is reachable yet — typically the very first agent on a fresh control-plane), the upstream copy is also published:
sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/lost-coder/panvex/main/deploy/install-agent.sh)"Requires a panel URL and enrollment token (create one in Settings → Enrollment Tokens).
📋 Non-interactive mode (CI / automation)
# Control Plane
PANVEX_ADMIN_PASS='<password>' \
PANVEX_HTTP_PORT=8080 \
PANVEX_GRPC_PORT=8443 \
sudo -E bash install.sh
# Agent
PANVEX_PANEL_URL='https://panel.example.com' \
PANVEX_ENROLLMENT_TOKEN='<token>' \
sudo -E bash install-agent.shRun bash install.sh --help for all environment variables.
┌─────────────────────────────────────────────────────┐
│ 🌐 Browser │
│ React · TanStack Router/Query │
├─────────────────────────────────────────────────────┤
│ 📡 Control Plane (:8080) │
│ HTTP API · WebSocket · Embedded UI │
├─────────────────────────────────────────────────────┤
│ 🔒 gRPC Gateway (:8443) │
│ mTLS · Bidirectional Stream · Jobs │
├─────────────────────────────────────────────────────┤
│ 🤖 Agent (per Telemt node) │
│ Heartbeats · Snapshots · Job Execution │
└─────────────────────────────────────────────────────┘
📁 Repository Layout
| Directory | Description |
|---|---|
cmd/control-plane |
Control plane server (HTTP + gRPC + embedded UI) |
cmd/agent |
Agent binary with bootstrap and enrollment |
internal/controlplane |
Auth, jobs, presence, storage, server logic |
internal/agent |
Telemt client, runtime, self-updater |
internal/gatewayrpc |
Generated gRPC stubs (protobuf) |
internal/security |
Enrollment, crypto, mTLS CA |
web |
React dashboard (Vite + TailwindCSS 4 + TanStack) |
db/migrations |
PostgreSQL and SQLite schema migrations |
proto |
Protobuf gateway contract |
deploy |
Install scripts, Docker Compose, nginx config |
🔧 Tech Stack
| Layer | Technology |
|---|---|
| Backend | Go 1.26, chi/v5, pgx/v5, modernc.org/sqlite, gRPC |
| Frontend | React 19, Vite 8, TailwindCSS 4, TanStack Router + Query |
| UI Kit | Inlined under web/src/ui/ — Radix UI primitives + CVA |
| Database | PostgreSQL (primary) · SQLite (lightweight) |
| Deploy | Multi-stage Docker · systemd · nginx |
Panvex reads its configuration from environment variables and a
config.toml file at startup. Operational tunables (password policy,
job worker cadences, presence thresholds, GeoIP, retention) are
edited at runtime via the dashboard and stored in the database.
| Layer | Source | Edited via |
|---|---|---|
| Bootstrap | PANVEX_* env / config.toml |
Edit and restart panel |
| Operational | DB | Settings → ⚙️ Sections |
| Per-user | DB (user_appearance) |
Settings → Appearance |
| Name | Required | Purpose |
|---|---|---|
PANVEX_STORAGE_DSN |
yes | sqlite path or postgres URL |
PANVEX_ENCRYPTION_KEY |
yes | master at-rest encryption key |
PANVEX_DB_PASSWORD |
postgres | overrides DSN password (keeps it out of files) |
PANVEX_HTTP_ADDR |
no | HTTP bind, default :8080 |
PANVEX_GRPC_ADDR |
no | gRPC bind, default :8443 |
PANVEX_TLS_MODE |
no | proxy (default) or direct |
PANVEX_TRUSTED_PROXY_CIDRS |
reverse-proxy | trust X-Forwarded-* from these CIDRs |
PANVEX_ENV |
no | production tightens cookie/HSTS defaults |
The full list of bootstrap variables (~26) lives in docs/settings/reference.md. A ready-to-edit example config is at docs/settings/example.config.toml.
Operational tunables (password lockout, session timeouts, presence thresholds, retention, GeoIP, update channel, plus 20 others) are managed through the dashboard at Settings. Changes take effect immediately; a few items (session timeouts) require a panel restart and the UI surfaces a banner when that's the case.
The same list is also visible at the
/api/settings/schema endpoint and
documented in docs/settings/reference.md.
Both binaries log via log/slog to stderr by default. Three controls
are available on the command line; each backend also accepts an env
fallback so deployments can configure without flag plumbing.
| Flag | Env | Binary | Values | Default | Effect |
|---|---|---|---|---|---|
-log-level |
PANVEX_LOG_LEVEL |
both | debug | info | warn | error |
info |
Minimum level emitted. |
-log-format |
PANVEX_LOG_FORMAT |
both | text | json |
text |
Output encoding. json is flat (no envelope) and ingests directly via Loki/Promtail/journald json parsers. |
-log-file |
PANVEX_LOG_FILE |
control-plane | path | none | When set, tees stderr to the file (append). Agent logs go to stderr only — daemonise via systemd / Docker. |
Both formats carry the same fields. The structured attributes per
layer (request_id, agent_id, attempt_id, job_id, …) follow the
conventions in docs/superpowers/logging.md;
new code should use the slog.*Context variants so the request-id
correlation works through HTTP and gRPC.
Typical recipes:
# Dev: verbose text on stderr
panvex-control-plane -log-level=debug
# Prod: JSON to stdout for Loki, plus a local rotation file
PANVEX_LOG_FORMAT=json PANVEX_LOG_FILE=/var/log/panvex/panel.log \
panvex-control-plane
# Agent under systemd: structured logs picked up by journald
panvex-agent -log-format=jsonThe control-plane also exposes a per-attempt enrollment timeline in the
dashboard (Server Detail → Enrollment history, plus the fleet-wide
/enrollment-attempts page) and a live "Recent events" section on the
Server Detail page that streams the agent's slog Info+ records in real
time over the existing gRPC connection.
go build ./... # Build all
go test ./... # Run tests
go test -race ./... # Race detector
golangci-lint run ./... # Lint
sqlc generate # Regenerate DB codecd web
npm install # Install deps
npm run dev # Dev server (proxies API to :8080)
npm run build # Production build
npm run lint # ESLintThree terms appear in different layers and mean related but distinct things:
| Term | Where you see it | Meaning |
|---|---|---|
| Server | Dashboard UI ("Servers" page) | Operator-facing label for the box being managed. |
| Node | Specs, design docs, fleet-group detail | Same thing as Server, used in lower-level technical copy. |
| Agent | DB tables, Go types, gRPC routes (agents, /api/agents/*) |
The panvex-agent Go process running on the node. One agent row ⇄ one node. |
When writing code, always use agent / agent_id. When writing user copy, prefer "Server".
The dev fleet (
scripts/dev-panel.sh,scripts/dev-agents.sh,scripts/dev-stop.sh) lives one level above this repo, in the workspace. It writes its DB, logs, and PIDs to.tmp/dev/. See the workspace scripts/README.md for the full orchestration loop. The flow below is the manual equivalent.
1. Bootstrap admin:
go run ./cmd/control-plane bootstrap-admin \
-username admin \
-password '<strong-password>'2. Start control plane:
PANVEX_STORAGE_DSN=data/panvex.db PANVEX_ENCRYPTION_KEY=<your-key> go run ./cmd/control-plane3. Start frontend dev server:
cd web && npm run devDashboard at
http://localhost:5173, API proxied to:8080
📦 Single binary build
cd web && npm run build:embed
cd .. && go build -tags embeddedui -o panvex-control-plane ./cmd/control-planeEach compose file ships a bootstrap service under --profile bootstrap
that creates the first admin one-shot. It refuses to plant an account
on a non-empty store, so it's safe to re-run.
SQLite (lightweight)
# 1. First-run admin (run before bringing up the backend so the SQLite
# file isn't contended).
PANVEX_BOOTSTRAP_PASSWORD='<strong-password>' \
docker compose -f deploy/docker-compose.sqlite.yml \
--profile bootstrap run --rm bootstrap
# 2. Start the stack.
docker compose -f deploy/docker-compose.sqlite.yml up --build -dPostgreSQL (dev — default password, plaintext DB traffic)
# 1. Start Postgres + backend (creates schema on first boot).
docker compose -f deploy/docker-compose.postgres.yml up --build -d
# 2. First-run admin.
PANVEX_BOOTSTRAP_PASSWORD='<strong-password>' \
POSTGRES_PASSWORD='<db-password>' \
docker compose -f deploy/docker-compose.postgres.yml \
--profile bootstrap run --rm bootstrapPostgreSQL (production — TLS, no default credentials)
# 1. Bring up the stack. Required env vars are enforced via ${VAR:?...}.
POSTGRES_PASSWORD='<strong-db-password>' \
PANVEX_ENCRYPTION_KEY='<strong-encryption-key>' \
docker compose -f deploy/docker-compose.prod.yml up --build -d
# 2. First-run admin. Reuse the same PANVEX_ENCRYPTION_KEY so freshly
# minted secrets are readable to the running backend.
POSTGRES_PASSWORD='<strong-db-password>' \
PANVEX_ENCRYPTION_KEY='<strong-encryption-key>' \
PANVEX_BOOTSTRAP_PASSWORD='<strong-admin-password>' \
docker compose -f deploy/docker-compose.prod.yml \
--profile bootstrap run --rm bootstrapThe prod profile refuses to start without POSTGRES_PASSWORD and
PANVEX_ENCRYPTION_KEY, forces sslmode=require, sets resource
limits, JSON-log rotation (15 MiB × 10), PANVEX_ENV=production, and
binds publishers to loopback (terminate TLS at a reverse proxy — see
deploy/nginx/default.conf).
Override the admin username via
PANVEX_BOOTSTRAP_USERNAME(default:admin).
PANVEX_GEOIP_DIRis an optional override for where auto/URL-mode GeoIP.mmdbfiles are written. Defaults to<dir(panvex.db)>/geoipfor SQLite deployments or/var/lib/panvex/geoipotherwise. Local-mode files are read from operator-supplied absolute paths and ignore this setting.Dashboard:
http://localhost:8080· gRPC:localhost:8443
Panvex supports two transport modes per agent:
- Inbound (default). The agent dials the panel. Use this when the panel is internet-reachable from the agent host.
- Outbound (reverse). The panel dials an agent that listens on a public host:port. Use this when the panel is firewalled (private network, VPN-only, behind NAT) but the agent has a public address.
The Add Server wizard (Dashboard → Servers → Add) generates the install command for either mode in one click.
- Create an enrollment token: Settings → Enrollment Tokens, or the wizard mints one for you.
- Paste the rendered command on the Telemt host:
sudo bash -c "$(curl -fsSL https://panel.example.com/install-agent.sh)"The script is embedded into the control-plane binary and served from the panel itself — no external CDN required.
Use POST /api/agents/provision-outbound (admin) or the wizard's
"Panel connects to agent" branch. The endpoint creates the agent row,
mints a 5-minute bootstrap token, and returns a pre-baked install
command pre-configured with --mode=reverse, --listen-addr=:<port>,
and the panel's CA pin. The agent listens; the panel's outbound
supervisor dials it via mTLS.
Each wizard run lets the operator pick one of two install-script sources:
| Source | URL | Integrity | Default for |
|---|---|---|---|
| Panel | <panel>/install-agent.sh |
SHA-256 self-check baked into the rendered curl command | Inbound |
| GitHub | https://raw.githubusercontent.com/lost-coder/panvex/main/deploy/install-agent.sh |
Pin a release tag in your deploy automation for reproducibility | Outbound |
Operators on a fork or behind a private mirror override the URLs via:
PANVEX_INSTALL_SCRIPT_URL=https://panel.example.com/install-agent.sh # panel source
PANVEX_INSTALL_SCRIPT_GITHUB_URL=https://github.example.com/raw/panvex/main/deploy/install-agent.shManual bootstrap (without installer)
./panvex-agent bootstrap \
-panel-url https://panel.example.com \
-enrollment-token '<token>' \
-state-file /var/lib/panvex-agent/agent-state.jsonCreate and manage Telemt clients centrally from the dashboard:
- 🔑 Generate secrets and
user_ad_tag - 📏 Set limits: connections, unique IPs, quota, expiration
- 🌐 Assign by fleet group or individual nodes
- 🔄 Rotate secrets without recreating the client
- 📈 Live deployment status, connection links, and usage per node
Two-Factor Authentication — TOTP 2FA is optional. Enable in Profile page.
Emergency TOTP reset via CLI:
./panvex-control-plane reset-user-totp \
-storage-driver sqlite \
-storage-dsn /var/lib/panvex/panvex.db \
-username admin| Subcommand | Purpose |
|---|---|
diagnose |
Markdown health snapshot — schema version, row counts, pool stats, CA expiry, encryption-key fingerprint. Paste into a support ticket. |
backup |
SQLite-only tar.gz of a VACUUM INTO snapshot plus metadata.json. Postgres operators use pg_dump. |
restore |
Prints the manual restore recipe (tar -xzf + migrate-schema). Auto-restore is intentionally not supported — overwriting a populated DB is the kind of mistake we refuse to make easy. |
verify-audit-chain |
Walks audit_events chronologically, recomputes the SHA-256 chain (migration 0038), exits non-zero on the first tampered or missing link. Run after a suspected incident or as part of a periodic compliance check. |
./panvex-control-plane diagnose \
-storage-driver sqlite \
-storage-dsn /var/lib/panvex/panvex.db
./panvex-control-plane backup \
-storage-driver sqlite \
-storage-dsn /var/lib/panvex/panvex.db \
-out /var/backups/panvex-$(date -u +%Y%m%dT%H%M%SZ).tar.gzThe encryption-key fingerprint embedded in both diagnose output and
metadata.json is a one-way SHA-256 prefix — operators can confirm two
panels share the same PANVEX_ENCRYPTION_KEY without ever exchanging
the key itself.
The control plane checks GitHub Releases for new versions automatically.
| Method | Command |
|---|---|
| Dashboard | Settings → Updates → Update Panel / Update Agent |
| CLI | ./panvex-control-plane self-update |
| Auto-update | Enable in Settings → Updates (disabled by default) |
Agents can be updated individually or in bulk. The panel sends an update job via gRPC — the agent downloads and installs the new binary automatically.
Built with ❤️ for Telemt fleet operators