Game server management dashboard for the ZeroHost platform. Provides user-facing server lifecycle management (creation, renewal, suspension, deletion) backed by a panel API.
Note
The dashboard is now in "cruise mode", instead of dozens of commits a day, expect around 2-3 commits per day going forward, as I'm juggling other projects on the side. That said, there will still be days with bigger pushes when needed.
Warning
What you'll see below in this README may no longer be current; the project is still undergoing intensive development and changes quite often, for example, the server has recently been rewritten in Go, so the deployment section of this README is no longer up to date. I haven't had time to update it yet, but I will eventually.
- Panel Compatibility
- Architecture Overview
- Tech Stack
- Project Structure
- Request Lifecycle
- Routing
- Database Schema
- Authentication Flow
- Server Lifecycle
- Scheduler
- Configuration
- Deployment
| Panel | Status |
|---|---|
| Pterodactyl | |
| Pyrodactyl | ✅ Tested — working |
| Hydrodactyl | ✅ Tested — working |
| Pelican | ❓ Not tested |
flowchart TB
subgraph Client["Client Layer"]
A["Browser (Vanilla JS SPA)"]
B["Cap CAPTCHA Widget"]
end
subgraph CDN["CDN Layer"]
C["jsDelivr\n(cap-widget)"]
D["Google Fonts"]
end
subgraph App["Application Layer (Express)"]
E["server.js\n(Entry Point)"]
F["middleware/auth.js\n(JWT Auth)"]
G["config/migrate.js\n(Schema Migration)"]
H["services/scheduler.js\n(Expiry Cron)"]
end
subgraph Routes["Route Handlers"]
I["routes/auth.js"]
J["routes/servers.js"]
end
subgraph Services["Service Layer"]
K["services/pyrodactyl.js\n(Panel API Wrapper)"]
L["services/activity.js\n(Activity Logger)"]
end
subgraph Config["Configuration"]
M["config/db.js\n(MariaDB Pool)"]
N["config/pyrodactyl.js\n(Panel Config & Limits)"]
O["config/cap.js\n(CAPTCHA Client)"]
end
subgraph External["External Systems"]
P["Pyrodactyl Panel\n(panel.zero-host.org)"]
Q["MariaDB"]
R["ip-api.com\n(VPN/Proxy Detection)"]
end
A -- "HTTP(S)" --> E
B -- "CAPTCHA Token" --> E
C -- "Web Component" --> A
D -- "Font Assets" --> A
E --> F
E --> G
E --> H
E --> I
E --> J
I --> K
I --> L
I --> O
J --> K
J --> L
J --> O
K --> P
K --> Q
L --> Q
I --> R
M --> Q
| Component | Technology |
|---|---|
| Runtime | Node.js >= 18 (ES Modules) |
| HTTP Framework | Express 4.21 |
| Database | MariaDB via mariadb driver 3.4 |
| Authentication | JWT (HS256, jsonwebtoken 9) |
| Password Hashing | Argon2id (argon2 0.41) |
| Frontend | Vanilla JavaScript (SPA, no framework) |
| Styling | Custom CSS |
| CAPTCHA | Self-hosted Cap CAPTCHA |
| Panel API | Pterodactyl / Pyrodactyl Application API |
| Process Manager | PM2 |
| CI/CD | GitHub Actions (SSH deploy) |
flowchart LR
subgraph Root["/"]
direction TB
sv["server.js\n(Express entry)"]
env[".env"]
pkg["package.json"]
pt["port.txt"]
end
subgraph Routes["routes/"]
auth["auth.js\nAuth endpoints"]
srv["servers.js\nServer CRUD"]
end
subgraph Middleware["middleware/"]
mw["auth.js\nJWT verification"]
end
subgraph Services["services/"]
p["pyrodactyl.js\nPanel API wrapper"]
a["activity.js\nActivity log"]
sc["scheduler.js\nExpiry cron"]
end
subgraph Config["config/"]
db["db.js\nMariaDB pool"]
mg["migrate.js\nSchema migrator"]
cp["cap.js\nCAPTCHA client"]
py["pyrodactyl.js\nPanel config"]
end
subgraph Public["public/"]
html["index.html\nShell"]
css["css/style.css\nStyles"]
js["js/app.js\nSPA frontend"]
end
subgraph GH["github/workflows/"]
beta["deploy-beta.yml"]
main["deploy-main.yml"]
end
Root --> Routes
Root --> Services
Root --> Config
Root --> Public
Root --> GH
sequenceDiagram
participant Browser
participant Express
participant Middleware
participant Handler
participant Service
participant Database
participant Pyrodactyl
Browser->>Express: HTTP Request
Express->>Express: helmet (security headers)
Express->>Express: cors
Express->>Express: rate-limit check
Express->>Middleware: authenticateToken (if /api/* except auth routes)
alt Valid Token
Middleware->>Middleware: jwt.verify(token, JWT_SECRET)
Middleware->>Express: req.user = { userId, email, username, pteroId }
Express->>Handler: Route handler
Handler->>Service: Business logic
Service->>Database: Query / Mutation
Service->>Pyrodactyl: Panel API call (if applicable)
Pyrodactyl-->>Service: Response
Database-->>Service: Result
Service-->>Handler: Processed result
Handler-->>Express: JSON Response
Express-->>Browser: 200 JSON
else Invalid / Expired Token
Middleware-->>Express: 401 / 403
Express-->>Browser: Error JSON
end
flowchart TD
A["/ or /overview\nDashboard Home"] --> B["Stat Cards\n(Total/Active/Slots/To-Renew)"]
A --> C["Resource Usage\n(RAM/CPU/Disk bars)"]
A --> D["Recent Servers\n(List of last 5)"]
A --> E["Recent Activity\n(Timeline)"]
F["/servers\nServer List"] --> G["Table with search & filter"]
G --> H["Settings Button"]
G --> I["Open Panel Button"]
G --> J["Renew Button"]
K["/server/:id\nServer Detail"] --> L["Tab: Info\n(egg, allocation, lifetime)"]
K --> M["Tab: Resources\n(live CPU/Memory/Disk gauges)"]
K --> N["Tab: Actions\n(Open, Reinstall, Delete)"]
O["/create\nCreate Server"] --> P["Name Input"]
O --> Q["Egg Selector\n(grouped by nest)"]
O --> R["Cap CAPTCHA"]
O --> S["Default Limits\n(512MB/50%/3GB)"]
T["/pyrodactyl\nPanel Redirect"] --> U["5-second auto-redirect\nto panel.zero-host.org"]
V["/account\nAccount Hub"] --> W["/account/edit\n(Email, Password, API Key)"]
V --> Y["/account/dangerous\n(Delete Account, RGPD Export)"]
Z["/log\nActivity Log"] --> AA["Paginated list\n(50 per page)"]
flowchart LR
subgraph Auth["/api/auth"]
direction TB
r["POST /register"]
l["POST /login"]
o["POST /logout"]
cp["POST /change-password"]
ce["POST /change-email"]
da["POST /delete-account"]
ed["GET /export-data"]
end
subgraph Servers["/api/servers"]
direction TB
ls["GET /list"]
ov["GET /overview"]
eg["GET /eggs"]
cr["POST /create"]
dt["GET /details/:id"]
rn["PATCH /:id\n(rename)"]
rw["POST /renew/:id"]
ri["POST /:id/reinstall"]
dl["DELETE /:id"]
rs["GET /resources/:identifier"]
ak["PUT /client-api-key"]
end
subgraph Other["/api"]
ac["GET /activity"]
hl["GET /health"]
end
erDiagram
users {
int id PK "AUTO_INCREMENT"
varchar email "NOT NULL"
varchar username "NOT NULL"
varchar password_hash "Argon2id hash"
int ptero_user_id "Pyrodactyl user ID"
varchar ptero_uuid "Pyrodactyl UUID"
varchar first_name
varchar last_name
tinyint password_set "DEFAULT 0"
varchar ptero_client_api_key "NULLABLE"
timestamp created_at "DEFAULT CURRENT_TIMESTAMP"
}
server_meta {
int id PK "AUTO_INCREMENT"
int ptero_server_id "NOT NULL"
int user_id "FK -> users"
timestamp created_at "DEFAULT CURRENT_TIMESTAMP"
timestamp expires_at "90d from creation/renewal"
enum status "'active' | 'suspended' | 'expired'"
}
user_ips {
int id PK "AUTO_INCREMENT"
int user_id "FK -> users ON DELETE CASCADE"
varchar ip_address "IPv4 or IPv6"
timestamp created_at "DEFAULT CURRENT_TIMESTAMP"
}
activity_log {
int id PK "AUTO_INCREMENT"
int user_id "NOT NULL"
varchar action "e.g. server_created"
varchar details "DEFAULT ''"
int server_id "NULLABLE"
timestamp created_at "DEFAULT CURRENT_TIMESTAMP"
}
users ||--o{ server_meta : "has"
users ||--o{ user_ips : "has"
users ||--o{ activity_log : "performs"
| Table | Index / Constraint | Type |
|---|---|---|
server_meta |
idx_expires (expires_at) |
Index |
server_meta |
idx_user (user_id) |
Index |
server_meta |
idx_status (status) |
Index |
user_ips |
idx_ip (ip_address) |
Index |
user_ips |
idx_user (user_id) |
Index |
user_ips |
fk_user_ips_user (user_id -> users.id) |
Foreign Key (CASCADE) |
activity_log |
idx_activity_user (user_id) |
Index |
activity_log |
idx_activity_created (created_at) |
Index |
| Table | Columns |
|---|---|
panel.egg_variables |
egg_id, name, env_variable, default_value, rules, description, user_viewable, user_editable |
sequenceDiagram
participant Browser
participant Express
participant Cap as "Cap CAPTCHA"
participant ipapi as "ip-api.com"
participant DB as "MariaDB"
participant Panel as "Pyrodactyl Panel"
Browser->>Express: POST /api/auth/register
Note over Browser,Express: Body: { email, username, password, capToken }
Express->>Cap: POST /siteverify (capToken)
Cap-->>Express: { success: boolean }
alt CAPTCHA Failed
Express-->>Browser: 400 { error: "CAPTCHA verification failed" }
end
Express->>ipapi: GET /json/{clientIP}
ipapi-->>Express: { proxy: boolean, ... }
alt Proxy / VPN Detected
Express-->>Browser: 403 { error: "VPN/Proxy detected" }
end
Express->>DB: SELECT IP count for client IP
DB-->>Express: count
alt 2+ accounts from same IP
Express-->>Browser: 403 { error: "Maximum 2 accounts per IP" }
end
Express->>Express: argon2.hash(password)
Express->>Panel: POST /api/application/users
Panel-->>Express: { attributes: { id, uuid } }
Express->>DB: INSERT INTO users (...)
DB-->>Express: OK
Express->>DB: INSERT INTO user_ips (...)
DB-->>Express: OK
Express->>Express: jwt.sign({ userId, email, username, pteroId })
Express->>DB: INSERT INTO activity_log (action='account_registered')
Express-->>Browser: 201 { token, user } + Set-Cookie: token
sequenceDiagram
participant Browser
participant Express
participant DB as "MariaDB"
participant ipapi as "ip-api.com"
Browser->>Express: POST /api/auth/login
Express->>Express: CAPTCHA verification
Express->>ipapi: VPN / Proxy check
ipapi-->>Express: result
alt Blocked
Express-->>Browser: 403
end
Express->>DB: SELECT * FROM users WHERE email = ?
DB-->>Express: user row
Express->>Express: argon2.verify(hash, password)
alt Invalid Password
Express-->>Browser: 401 { error: "Invalid credentials" }
end
Express->>Express: jwt.sign({ userId, email, username, pteroId })
Express-->>Browser: 200 { token, user } + Set-Cookie: token
stateDiagram-v2
[*] --> Creating: POST /api/servers/create
Creating --> Active: Pyrodactyl API success
Creating --> Failed: API error
Active --> Suspended: Scheduler (expired)
Active --> Suspended: Manual suspend (Pyrodactyl)
Active --> Renewed: POST /api/servers/renew/:id
Active --> Deleted: DELETE /api/servers/:id
Suspended --> Active: Renew (within 7d window)
Suspended --> Expired: 7 days past expiry
Suspended --> Deleted: DELETE /api/servers/:id
Renewed --> Active: expires_at += 90 days
Expired --> [*]
Failed --> [*]
Deleted --> [*]
| Resource | Value |
|---|---|
| RAM | 512 MB |
| CPU | 50% |
| Disk | 3 GB |
| Swap | 0 |
| Backups | 1 |
| Allocations | 1 |
- Servers expire 90 days after creation or last renewal.
- Renewal is only permitted within a 7-day window before or after the expiration date.
- Renewing an expired server automatically unsuspends it.
The scheduler (services/scheduler.js) runs daily at midnight via setInterval:
flowchart TD
A["Scheduler Start"] --> B["Hourly check:\nis it midnight?"]
B -->|Yes| C["SELECT * FROM server_meta\nWHERE expires_at < NOW()\nAND status = 'active'"]
C --> D["For each expired server:"]
D --> E["POST /api/application/servers/{id}/suspend"]
E --> F["UPDATE server_meta\nSET status = 'suspended'"]
F --> G["INSERT INTO activity_log\naction = 'server_suspended'"]
B -->|No| B
| Variable | Description |
|---|---|
JWT_SECRET |
Secret key for JWT signing (HS256) |
JWT_EXPIRES_IN |
Token expiry duration (default: 2h) |
COOKIE_SECRET |
Secret for signed cookies |
DB_HOST |
MariaDB host |
DB_USER |
MariaDB user |
DB_PASSWORD |
MariaDB password |
DB_NAME |
MariaDB database name |
DB_PORT |
MariaDB port (default: 3306) |
PTERO_URL |
Pyrodactyl panel base URL |
PTERO_API_KEY |
Pyrodactyl Application API key |
CAP_ENDPOINT |
Cap CAPTCHA endpoint URL |
CAP_SECRET |
Cap CAPTCHA secret key |
NODE_ENV |
production or development |
| Scope | Window | Max Requests |
|---|---|---|
| Auth endpoints (login, register) | 15 minutes | 10 |
| General API | 60 seconds | 100 |
flowchart LR
subgraph Dev["Development"]
A["git push beta"]
end
subgraph CI["GitHub Actions"]
B["Checkout"]
C["Install deps\n(npm ci)"]
D["SSH Deploy"]
E["PM2 Restart"]
end
subgraph Prod["Production Server"]
F["Pull from beta/main"]
G["npm install --production"]
H["PM2 reload"]
I["DB migration\n(auto on startup)"]
end
A --> B --> C --> D --> E
E --> F --> G --> H --> I
Two deployment workflows are configured:
- deploy-beta.yml: Triggered on push to
betabranch. - deploy-main.yml: Triggered on push to
mainbranch.
Both use SSH credentials configured as GitHub repository secrets (SSH_HOST, SSH_USER, SSH_PASSWORD, SSH_PORT).