REST API behind the OTC Status Dashboard. It stores components and the events that affect them — incidents, maintenance windows and informational messages — and exposes them to the frontend, to machine reporters and as an RSS feed.
Go 1.26 · Gin · GORM/PostgreSQL · OIDC (Zitadel), shipped as a small Alpine image (Dockerfile).
cp .env.example .env # fill in the Zitadel issuer, client id and role names
docker compose up -d database
make migrate-up
go run cmd/main.go # or: make build && ./appThe API then listens on SD_PORT (8000 by default). Swagger UI is at /swagger/index.html and the
raw specification at /openapi.json.
Read from the environment, with a .env file as fallback (internal/conf). Everything is prefixed
with SD_. The application refuses to start when SD_PORT, the OIDC pair or SD_RBAC_ROLES_ADMINS
is missing or invalid.
| Variable | Default | Notes |
|---|---|---|
SD_DB |
– | PostgreSQL DSN, e.g. postgres://pg:pass@localhost:5432/status_dashboard?sslmode=disable |
SD_CACHE |
– | cache backend, internal for the in-process implementation |
SD_LOG_LEVEL |
devel |
devel or a zap level such as info, warn, error |
SD_PORT |
8000 |
must be between 1024 and 50000 |
SD_OPENAPI_SPEC_PATH |
openapi.yaml |
path of the spec served at /openapi.json |
SD_OIDC_ISSUER |
– | required, must match the discovered issuer exactly |
SD_OIDC_CLIENT_ID |
– | required, the audience accepted in aud |
SD_OIDC_USERNAME_CLAIM |
– | display name only, never used as identity |
SD_RBAC_ROLES_ADMINS |
– | required, role name(s) mapped to admin |
SD_RBAC_ROLES_OPERATORS |
– | role name(s) mapped to operator |
SD_RBAC_ROLES_CREATORS |
– | role name(s) mapped to creator |
SD_RBAC_ROLES_REPORTERS |
– | role name(s) mapped to reporter (machine principals) |
SD_STATIC_ORIGINS |
– | OBS website endpoints serving the static site, primary first, host only |
SD_STATIC_CACHE_TTL |
5m |
lifetime of a cached response that carries no Cache-Control |
SD_STATIC_CACHE_MAX_BYTES |
67108864 |
byte budget of the whole response cache |
SD_NOTIFICATIONS_ENABLED |
false |
master switch for maintenance email notifications |
SD_SMTP_HOST / SD_SMTP_PORT / SD_SMTP_FROM |
– | required when notifications are enabled |
SD_SMTP_USER / SD_SMTP_PASSWORD / SD_SMTP_TLS |
– | optional SMTP AUTH and TLS settings |
SD_SMTP_TIMEOUT |
30s |
SMTP connect/send timeout (Go duration) |
SD_NOTIFICATIONS_LEASE_TIMEOUT |
60s |
must exceed SD_SMTP_TIMEOUT |
SD_NOTIFICATIONS_MAX_ATTEMPTS |
5 |
retry limit before a row is marked failed |
SD_NOTIFICATIONS_BACKOFF_INTERVAL |
5m |
base delay for exponential retry backoff |
SD_NOTIFICATIONS_SMOD_EMAIL |
– | SMOD review recipient |
SD_NOTIFICATIONS_EMAILS_OPERATORS |
– | comma-separated operator review recipients |
SD_NOTIFICATIONS_EMAILS_ADMINS |
– | comma-separated admin review recipients |
SD_NOTIFICATIONS_ALLOWED_DOMAINS |
– | domain allow-list for contact_email; empty accepts all |
SD_NOTIFICATIONS_EXCLUDED_EMAILS |
– | addresses that never receive notifications |
SD_WEB_URL |
– | web origin for maintenance deep links in emails |
SD_METRICS_PORT |
9090 |
dedicated /metrics port (only started when notifications are enabled) |
Each SD_RBAC_ROLES_* variable accepts a comma-separated list of names. See docs/auth.md.
Maintenance email notifications are documented in docs/notifications.md.
openapi.yaml is the authoritative contract; it is baked into the image and served with the API.
| Group | Endpoints |
|---|---|
/v2/events |
current event API: list with pagination, create, read, patch, patch an update, extract components |
/v2/incidents |
deprecated alias of /v2/events |
/v2/components |
list, create, read |
/v2/availability |
monthly availability per component |
/v2/notifications |
admin-only queue stats, failed rows and re-drive (see docs/notifications.md) |
/rss/ |
RSS feed for the frontend |
Business rules the specification cannot express — validation, status lifecycles, automatic transitions — are documented in docs/events.md.
With SD_STATIC_ORIGINS set, every path the API does not own is proxied to the OBS website
endpoints over HTTPS, with the origin hostname in Host because OBS selects the bucket from that
header and ignores SNI. Origins are tried in order: a connect failure, a timeout or a 502/504
falls through to the next one. GET and HEAD are proxied, any other method keeps the API's 404,
and an empty origin list leaves the router exactly as it was without this feature.
Responses are cached in memory per method, host, path and query:
GETonly — neverHEAD, never an API route.- TTL from
max-age/s-maxage,SD_STATIC_CACHE_TTLwhen the origin sends neither; responses markedno-storeorno-cacheand responses that are not200are not stored. - A single object is buffered up to 8 MiB, the whole cache is bounded by
SD_STATIC_CACHE_MAX_BYTESand evicted least-recently-used first. - Concurrent requests for the same missing entry share one origin fetch, and an expired entry is served when every origin is unreachable.
X-Cache reports the outcome: HIT, MISS, STALE or BYPASS.
make test # unit tests
make test-acc # integration tests (Docker required)
make lint # golangci-lint 2.11.4
make migrate-up # apply the next migration from db/migrations
make migrate-create name=add_xyzLayout: cmd/ entry point, internal/api HTTP layer (middleware, routes, v2, rbac,
auth), internal/static OBS proxy and response cache, internal/db persistence,
internal/checker background status transitions, internal/event domain types,
db/migrations schema, tests/ integration suite, openapi.yaml contract.
| Document | Contents |
|---|---|
| docs/events.md | event types, creation rules, status lifecycles, availability, RSS |
| docs/auth.md | OIDC authentication, RBAC roles, permissions, field visibility |
| docs/notifications.md | maintenance email notifications: outbox, recipients, config, ops |
| docs/testing.md | unit and integration suites, how to run them |
| docs/diagrams/ | decision graph for system incident creation |
| openapi.yaml | HTTP contract and schemas |