Reviewed-on: #20
Catalyst
A self-hosted infrastructure registry. Track every VM, container, and service across your homelab — their state, stack, and which internal services are running on them.
Features
- Dashboard — filterable, searchable instance list with state and stack badges
- Detail pages — per-instance view with service flags, Tailscale IP, and timestamps
- 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
- REST API — every operation is a plain HTTP call; no magic, no framework lock-in
- Persistent storage — SQLite database 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 run -d \
--name catalyst \
-p 3000:3000 \
-v catalyst-data:/app/data \
gitea.thewrightserver.net/josh/catalyst:latest
Or with the included Compose file:
docker compose up -d
Open http://localhost:3000.
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 or vmid |
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": 0, "semaphore": 0,
"patchmon": 1, "tailscale": 1, "andromeda": 0,
"hardware_acceleration": 1,
"created_at": "2024-01-15T10:30:00.000Z",
"updated_at": "2024-03-10T14:22:00.000Z"
}
]
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 |
404 |
No instance with that VMID |
400 |
VMID is not a valid integer |
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, unique |
state |
string | yes | deployed, testing, or degraded |
stack |
string | yes | production or development |
tailscale_ip |
string | no | Defaults to "" |
atlas |
0|1 | no | Defaults to 0 |
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 |
404 |
No instance with that VMID |
422 |
Instance is on the production stack |
400 |
VMID is not a valid integer |
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 — all CRUD operations, constraints, filters |
tests/api.test.js |
HTTP API — all endpoints, status codes, error cases |
tests/helpers.test.js |
UI helper functions — esc() XSS contract, fmtDate() |
Versioning
Catalyst uses semantic versioning. package.json is the single source of truth for the version number.
| Change | Bump | Example |
|---|---|---|
| Bug fix | patch | 1.0.0 → 1.0.1 |
| New feature, backward compatible | minor | 1.0.0 → 1.1.0 |
| Breaking change | major | 1.0.0 → 2.0.0 |
Cutting a release
# 1. Bump version in package.json, then:
git add package.json
git commit -m "chore: release v1.1.0"
git tag v1.1.0
git push && git push --tags
Pushing a tag triggers the full CI pipeline: test → build → release.
- Docker image tagged
:1.1.0,:1.1, and:latestin the Gitea registry - A Gitea release is created at
v1.1.0