10
Check types per service
52
Releases since April 2026
251
Automated tests
3
Roles: admin, manager, viewer
In this case study
The problem
I operate infrastructure for myself and for other people: web servers, mail, DNS, WireGuard gateways, Raspberry Pis, TP-Link Omada networks and point-of-sale hardware. Every audience wanted its own status page with its own name, colours and domain, and none of them should see each other’s servers.
Hosted status-page products price per page or per seat, hold your uptime data on their servers, and rarely understand network gear or POS devices. Running one open-source monitor per client multiplies upgrades, backups and logins.
Goal: a single self-hosted deployment that serves unlimited branded status pages, understands the protocols and devices I actually run, and communicates incidents to customers automatically.
Architecture
The whole platform is one Node.js process in front of MariaDB, shipped as a container. Keeping it to a single service was deliberate: it is what makes upgrades a two-command affair and backups a single volume.
Poller
Runs every service’s checks on its own interval, records latency and outcome, and drives incident state transitions.
Express app
Admin console, public dashboards, JSON feeds, SSE live state, SVG badges and health endpoints from one server.
MariaDB
Services, checks, 90-day history, 180-cycle heartbeats, incidents, maintenance, users, audit and API keys.
Reverse proxy
Caddy terminates TLS. The app picks the dashboard from the Host header, so a custom domain is a DNS change.
Public pages live at /dashboard/<slug> and can be mapped to a custom domain. Each ships an installable web-app manifest that is intentionally network-only: a status page must never show stale cached state. The app is server-rendered EJS with vanilla CSS and JavaScript, no front-end build step, on Node.js 22 or newer.
What it provides
Monitoring
Several checks per service, per-service polling intervals and failure thresholds, latency tracking, certificate expiry, 90 days of check history and a 180-cycle heartbeat strip, plus monitoring pause and flap detection.
Public status pages
Branded dashboards with custom domains, light and dark themes, service health, incident and maintenance history, announcements, RSS, email subscriptions, layout presets, per-group custom CSS and an installable PWA per dashboard.
Operations
An enterprise overview with attention queue, on-page workspaces for every management area, live logs, audit history, maps with geographic service locations, private Markdown runbooks, JSON import and export, SLA targets and health endpoints.
Communication
Automated and manual incidents with operator updates, public or private visibility and impact levels; planned maintenance; global or dashboard-scoped alert channels with quiet hours; weekly email reports.
Integrations
Omada SDN, UniFi Network, Square POS, Google OAuth, Cloudflare Turnstile, SMTP, Mapbox or MapLibre maps, generic webhooks, and Discord, Slack, Teams, Telegram, Pushover and ntfy alert formats.
Automation
Instance-wide read or write API keys, an external push-status endpoint for services the poller cannot reach, visitor-safe JSON feeds, Server-Sent Events for live state, and embeddable SVG badges for status, uptime, latency and certificate expiry.
Security controls
bcrypt password hashing, database-backed sessions, secure cookies behind HTTPS, endpoint-specific rate limits, security headers and CSP, parameterised SQL, per-dashboard grants, anonymous response allowlists, dependency scanning and a non-root container.
Backup and restore
A bundled script writes gzip-compressed MariaDB dumps with seven days of local retention; restore is confirmation-gated because the dump holds credentials, sessions and API-key hashes as well as history.
The check engine
A service can run several checks in the same poll cycle, each with a polling interval from 10 to 3,600 seconds and a failure threshold of 1 to 10 consecutive misses. Latency-reporting checks feed a 24-hour response-time chart, so degradation shows up before downtime does.
ICMP ping
Reachability and round-trip latency.
TCP port
Connection and connect time.
UDP port
Reachability for services like WireGuard.
HTTP / HTTPS
Status, response time, required or forbidden body text, TLS inspection.
DNS record
A, AAAA, CNAME, MX, TXT, NS with expected values.
TLS certificate
Expiry with a configurable warning threshold.
Omada
Gateway/WAN, LTE and AP/switch health via Open API v6.
UniFi
Gateway, AP/switch, WAN subsystem and minimum client count.
Square POS
Location or device availability with delayed outage confirmation.
Script
Admin-only command check; exit code decides health.
The Square check is a good example of learning from real alerts: POS devices briefly drop off Wi-Fi all the time, so outages are confirmed after a delay instead of paging on the first miss. Controller checks reject loopback, private and link-local targets by default, and an allow-list can restrict controller hostnames further.
Tenancy and access control
Dashboards are the tenancy boundary. A service can appear on several dashboards; a viewer account is granted specific dashboards and only ever sees, edits or receives alerts for those.
| Capability | Administrator | Manager | Viewer |
|---|---|---|---|
| Browse management data | All dashboards and services | All operational data | Assigned dashboards only |
| Add, edit, reorder services | Yes | Yes | Within assigned dashboards |
| Delete services | Yes | No | No |
| Incidents, banners, status pages | Yes | Yes | Read public incidents |
| Dashboard branding and settings | Full, including service assignment | Yes | Branding only |
| Alert channels | Global or per dashboard | Yes | Assigned-dashboard scope |
| Omada, UniFi, Square resources | All | Yes | Scoped to assigned dashboards |
| Users, invites, API keys, audit, settings | Yes | No | No |
The manager role arrived in v3.13 together with one-use, time-limited invite links, so a teammate can create their own account with the right role and no admin involvement.
Public status pages are visitor-facing by design. Viewer grants control what a user can manage, not who can read the public route; anonymous and viewer feeds omit private incidents and internal infrastructure details.
Incidents, maintenance and alerts
Incidents open and close themselves
Probe transitions automatically open and resolve incidents. Operators then refine what customers see: a custom title, minor, major or critical impact, public or private visibility, and investigating, identified, monitoring and resolved updates on a timeline.
Maintenance suppresses noise, not monitoring
A maintenance window can cover many services. The app stores one window per affected service, groups matching windows on public pages, and suppresses outbound status-change alerts while a service is in maintenance. Checks and history keep running so the record stays complete.
Alerts where the team already is
Channels can be global or scoped to a single dashboard, with Generic JSON, Discord, Slack and email payloads. URL detection also covers Microsoft Teams, Telegram, Pushover and ntfy. Public visitors can subscribe by email to down and recovery notices, and operators get weekly email reports.
From v3.4 to v3.16: what changed since
The first version of this study ended at v3.4.0 in April. Between May and September the project shipped another 25 releases. The pattern is the same as the earlier ones: a burst of features, then a week of production-driven fixes.
- May · v3.5 to v3.6 — More things to watchUniFi Network Controller support alongside Omada, with WAN health, client-count and device checks; a dedicated TLS certificate-expiry check; ntfy push alerts; response-time sparklines on every card; and a version badge in the top bar.
- August · v3.6.5 to v3.7 — The public page grows upA public-first health overview with search, filters and a status key; a visitor theme toggle; upcoming-maintenance summaries; an RSS feed per dashboard combining incidents and maintenance; and the HTTP server now binds before the first poll so a slow check can never block start-up.
- August · v3.8 to v3.10 — Enterprise operations workspaceEvery admin destination became a permanent searchable list-and-detail workspace on one page, with responsive layouts, an enterprise design system, and a hardened anonymous serializer that strips addresses, coordinates and runbooks from public payloads. Viewer access was tightened to assigned dashboards everywhere.
- August 18 · v3.10.2 to v3.16 — Ten releases in one daySLA targets and an SLA dashboard, per-group custom CSS, a real Content Security Policy, 90-check sparklines, monitoring pause, flap detection, bulk actions, response-time badges, incident auto-resolve, alert quiet hours, service reordering, the manager role with invite links, status-page design presets, a ⌘K global search, inline status changes, manual incident creation and resolve-all. Test count went from 75 to 228.
- September · v3.16.1 to v3.16.4 — Production hardeningA mysql2 security upgrade forced through the whole dependency tree, prototype-pollution and request-forgery fixes from CodeQL, a CSP directive that had silently blocked every inline click handler, and a database-resilience overhaul after the 30-day uptime query started scanning a 5.4-million-row table on every admin request.
Decisions that shaped the product
Removed a feature
High availability was cut in v3.4.0. Bidirectional MariaDB replication, a promote webhook, a split-brain guard and a Cloudflare Load Balancer kept two monitors alive, and the operational cost was out of proportion for a self-hosted monitor. Simplifying back to a single server changed no configuration and needed no downtime to upgrade.
Security hardening
API keys are stored as HMAC-SHA256 with the session secret as the pepper, key names are HTML-escaped, and the public subscribe endpoint was rewritten to be ReDoS-safe after a review of regex-driven input handling.
Observability first
Health is a first-class endpoint. /healthz answers liveness plus database connectivity for Docker; /health reports version, uptime, last poll age and service count; ?strict=1 additionally requires at least one configured service, which makes it a safe target for an external uptime monitor or load balancer.
Licensing
Source-available rather than open source. Personal and internal non-commercial use is free; modification, redistribution and commercial use need written permission. The code is fully readable, which matters for something that holds your infrastructure map.
Learned in production
Never aggregate on the request path. In v3.16.3 the admin API hung after a few minutes of use: a 30-day uptime figure was computed inline on every server-list request, its date filter could not use the history index, and one scan took 156 seconds on 5.4 million rows. Ten overlapping scans exhausted the pool. The fix was a per-day rollup table maintained with one tiny upsert per poll, a background refresh the route only reads from memory, a covering index built online, a 60-second cap on every pool query, and a watchdog that replaces a wedged pool without a container restart. The follow-up, v3.16.4, made the backfill non-locking so it could never stall the poller either.
The click that did nothing
Security defaults can be invisible breakage. Enabling a real Content Security Policy in v3.10.3 merged Helmet’s default script-src-attr 'none' into the header. Pages rendered and logged nothing, but every inline click handler in the admin was refused. v3.16.2 sets the directive explicitly and adds tests that render the real Helmet config and assert the default never returns.
Deployment, security and operations
The published image applegater/status-server runs as the non-root node user on linux/amd64 and linux/arm64, listens on port 3000, needs only NET_RAW for ICMP, and upgrades its own schema at start-up. Production refuses to start with the fallback session secret or a missing database password.
git clone https://github.com/X4Applegate/status-server.git
cd status-server
cp docker-compose.example.yml docker-compose.yml
# set SESSION_SECRET, DB_PASSWORD, EXTERNAL_URL, TZ
docker compose pull && docker compose up -d
curl --fail http://localhost:3000/healthz
Upgrades are a backup, a pull and a recreate of the app service. The session secret must stay stable across upgrades because it also peppers API-key hashes. In front of it, Caddy needs two lines per hostname, and each custom dashboard domain is just another site block pointing at the same container. Since v3.16.3 every pool query is capped, MariaDB kills runaway statements server-side, and /healthz reports query timeouts and pool recreations so an external monitor can see database trouble before users do.
Richard Applegate