Case study

Status Server — One Deployment, Many Branded Status Pages

Multi-tenant 10 check types Omada · UniFi · Square amd64 + arm64 Source-available

10

Check types per service

52

Releases since April 2026

251

Automated tests

3

Roles: admin, manager, viewer

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.

CapabilityAdministratorManagerViewer
Browse management dataAll dashboards and servicesAll operational dataAssigned dashboards only
Add, edit, reorder servicesYesYesWithin assigned dashboards
Delete servicesYesNoNo
Incidents, banners, status pagesYesYesRead public incidents
Dashboard branding and settingsFull, including service assignmentYesBranding only
Alert channelsGlobal or per dashboardYesAssigned-dashboard scope
Omada, UniFi, Square resourcesAllYesScoped to assigned dashboards
Users, invites, API keys, audit, settingsYesNoNo

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.