Reviewed-on: #46
Catalyst
A self-hosted infrastructure registry for homelab Proxmox environments. Track virtual machines across stacks, monitor service health, and maintain a full audit log of every configuration change.
Features
- Dashboard — filterable, searchable instance list with state and stack badges
- Detail pages — per-instance view with service flags, Tailscale IP, and a full change timeline
- Audit log — every field change is recorded with before/after values and a timestamp
- Full CRUD — add, edit, and delete instances via a clean modal interface
- Production safeguard — only development instances can be deleted; production instances must be demoted first
- Export / import — JSON backup and restore via the settings modal
- REST API — every operation is a plain HTTP call
- Persistent storage — SQLite on a Docker named volume; survives restarts and upgrades
- Zero native dependencies — SQLite via Node's built-in
node:sqlite; no compilation, no binaries
Quick start
docker compose up -d
Open http://localhost:3000.
Environment variables
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
HTTP port the server binds to |
DB_PATH |
data/catalyst.db |
Path to the SQLite database file |
REST API
All endpoints are under /api. Request and response bodies are JSON.
Instances
GET /api/instances
Returns all instances sorted by name. All query parameters are optional.
| Parameter | Type | Description |
|---|---|---|
search |
string | Partial match on name, vmid, or stack |
state |
string | Exact match: deployed, testing, degraded |
stack |
string | Exact match: production, development |
GET /api/instances?search=plex&state=deployed
[
{
"vmid": 117,
"name": "plex",
"state": "deployed",
"stack": "production",
"tailscale_ip": "100.64.0.1",
"atlas": 1, "argus": 1, "semaphore": 0,
"patchmon": 1, "tailscale": 1, "andromeda": 0,
"hardware_acceleration": 1,
"created_at": "2024-01-15T10:30:00",
"updated_at": "2024-03-10T14:22:00"
}
]
GET /api/instances/stacks
Returns a sorted array of distinct stack names present in the registry.
GET /api/instances/stacks
→ ["development", "production"]
GET /api/instances/:vmid
Returns a single instance by VMID.
| Status | Condition |
|---|---|
200 |
Instance found |
400 |
VMID is not a valid integer |
404 |
No instance with that VMID |
GET /api/instances/:vmid/history
Returns the audit log for an instance — newest events first.
| Status | Condition |
|---|---|
200 |
History returned (may be empty array) |
400 |
VMID is not a valid integer |
404 |
No instance with that VMID |
[
{
"id": 3,
"vmid": 117,
"field": "state",
"old_value": "testing",
"new_value": "deployed",
"changed_at": "2024-03-10T14:22:00"
},
{
"id": 1,
"vmid": 117,
"field": "created",
"old_value": null,
"new_value": null,
"changed_at": "2024-01-15T10:30:00"
}
]
POST /api/instances
Creates a new instance. Returns the created record.
| Status | Condition |
|---|---|
201 |
Created successfully |
400 |
Validation error — see errors array in response |
409 |
VMID already exists |
Request body:
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | |
vmid |
integer | yes | Must be > 0 and unique |
state |
string | yes | deployed, testing, or degraded |
stack |
string | yes | production or development |
tailscale_ip |
string | no | Valid IPv4 or empty string |
atlas |
0|1 | no | |
argus |
0|1 | no | |
semaphore |
0|1 | no | |
patchmon |
0|1 | no | |
tailscale |
0|1 | no | |
andromeda |
0|1 | no | |
hardware_acceleration |
0|1 | no |
PUT /api/instances/:vmid
Replaces all fields on an existing instance. Accepts the same body shape as POST. The vmid in the body may differ from the URL — this is how you change a VMID.
| Status | Condition |
|---|---|
200 |
Updated successfully |
400 |
Validation error |
404 |
No instance with that VMID |
409 |
New VMID conflicts with an existing instance |
DELETE /api/instances/:vmid
Deletes an instance. Only instances on the development stack may be deleted.
| Status | Condition |
|---|---|
204 |
Deleted successfully |
400 |
VMID is not a valid integer |
404 |
No instance with that VMID |
422 |
Instance is on the production stack |
Backup
GET /api/export
Downloads a JSON backup of all instances as a file attachment.
{
"version": 1,
"exported_at": "2024-03-10T14:22:00.000Z",
"instances": [ ... ]
}
POST /api/import
Replaces all instances from a JSON backup. Validates every row before committing — if any row is invalid the entire import is rejected.
| Status | Condition |
|---|---|
200 |
Import successful — returns { "imported": N } |
400 |
Body missing instances array, or validation errors |
Development
npm install
npm test # run all tests once
npm run test:watch # watch mode
npm start # start the server on :3000
Tests are split across three files:
| File | What it covers |
|---|---|
tests/db.test.js |
SQLite data layer — CRUD, constraints, filters, history logging |
tests/api.test.js |
HTTP API — all endpoints, status codes, error cases |
tests/helpers.test.js |
UI helpers — esc() XSS contract, date formatting, history formatters |
Versioning
Catalyst uses semantic versioning. package.json is the single source of truth.
| Change | Bump |
|---|---|
| Bug fix | patch |
| New feature, backward compatible | minor |
| Breaking change | major |
Pushing a tag triggers the CI pipeline: test → build → release.
Docker images are tagged :x.y.z, :x.y, and :latest.