Backend (Go) for homepad — the self-hosted homelab launcher with live uptime badges. Serves the shared service catalog, per-user favorites & ordering, admin catalog CRUD, and both auth paths; polls Gatus server-side so the browser never sees the Gatus URL.
Web frontend (React + Vite) lives at homepad
— see its README for the banner, screenshots, and rendered architecture / auth
diagrams.
Spec:
homepad/specs/v1-launcher.mdTest plan:homepad/specs/test-plan-v1.md
✅ Alpha-complete. Every acceptance criterion this repo owns (A1, A4, A5, A6,
A9, A10, A11-backend) is implemented and green against real Postgres — no 501
stubs remain. go test ./... runs 26 tests green; go vet ./... clean.
- Auth — local register / login / logout (bcrypt, in-memory sessions) and
PocketID OIDC (Authorization Code + PKCE, ID-token verified on stdlib crypto,
account-link by email, admin role from the OIDC group). OIDC is fully additive
and only active when
OIDC_ENABLED=true. - Catalog — shared service list; order-aware
GET /api/servicesmerges each user's saved layout. Admin-only create / edit / delete (non-admin → 403). - Per-user state — favorites and personal sort order, persisted in Postgres.
- Status poller — polls Gatus on an interval (≤30s, status
as_ofexposed) and serves status per service; Gatus unreachable → allUNKNOWN, never a 5xx. - A11 — the Gatus URL is never present in any API response.
POST /api/register GET /api/me
POST /api/login POST /api/logout
GET /api/auth/config GET /api/auth/oidc/login (when OIDC on)
GET /api/auth/oidc/callback (when OIDC on)
GET /api/services POST /api/services (admin)
PUT /api/layout PATCH /api/services/{id} (admin)
DELETE /api/services/{id} (admin)
GET /api/status POST /api/status/refresh (re-poll Gatus now)
GET /api/services carries responseTimeMs per service — the latest Gatus
check's response time — only when there is one; the key is omitted (never
0) for unmonitored services or when Gatus reported no duration.
A service whose latest check succeeded but took longer than GATUS_DEGRADED_MS
(default 1000; 0 disables) reads DEGRADED — "Slow" on the tile. Gatus has
no degraded state of its own; homepad derives it from the reported duration.
Admins can change the threshold at runtime from the System panel: it is
statusDegradedMs on GET /api/system/config (the effective value) and
PATCH /api/admin/settings (migration 0014); a saved value wins over the env
var and applies to the next poll with no restart.
POST /api/status/refresh re-polls Gatus synchronously: 200 {as_of} when it
answered, 503 {error, as_of} when it could not be reached (the last good
snapshot and its as_of stand).
GET /api/me / PATCH /api/me carry densityPref (large|compact|list, default
compact) alongside themePref. PATCH takes either or both fields; only the
fields present are validated and written.
categories.gridWidth is a 12-column span — 3 (quarter), 4 (third), 6
(half) or 12 (full); default 6. PATCH /api/categories/{id} {gridWidth} rejects
anything else with 400. Migration 0013 remapped the old 1–8 tile counts
(1→3, 2→4, 3→6, 4→6, 5→12, 6–8→12) and kept the old value in grid_width_legacy.
cmd/homepad-api/ entry point (opens Store, migrates, starts Poller)
internal/api/ HTTP router + handlers (+ integration tests)
internal/oidc/ PocketID OIDC: discovery, PKCE, ID-token verify, JWKS
internal/gatus/ Gatus client + status poller
internal/session/ in-memory session manager (v1)
internal/storage/ Postgres access + migrations
internal/testsupport/ test harness (httptest server, Gatus + IdP stubs)
migrations/ SQL migrations
cp .env.example .env
make db-up # docker compose Postgres on :5432
make run # boots on :8080make test # full suite (Postgres tests skipped if DATABASE_URL unset)
make test-unit # fast subset
make test-integration # spins Postgres + runs everythingK8s manifests are owned by Joe (homie / SRE bot), not in this repo. This repo ships:
Dockerfile(multi-stage, distroless final image, nonroot).env.example(the full env contract, incl. theOIDC_*values)specs/v1-launcher.md§ Deployment contract (canonical: image, ports, env, secrets, probes)