Skip to content

Repository files navigation

Better Status Page

A self-hosted status page that doesn't make you cry. 🟢

Monitor your services. Alert your team. Keep your users in the loop. No cloud. No subscription. No drama.

Node.js TypeScript React Fastify License: MIT


The pitch 🎤

You know the drill. Your API goes down at 3 AM, users start tweeting, your boss is calling, and your status page is... a static HTML file someone last updated in 2019 that says "All systems operational" in Comic Sans.

BetterStatusPage fixes that. It's a fully self-hosted monitoring and status page platform that runs as a single Node.js process with zero external dependencies — no Postgres, no Redis, no Kubernetes, no $200/month SaaS bill. Just clone, configure, deploy. Done.


Release status

BetterStatusPage is currently in its MVP phase. The current target is a practical self-hosted deployment for small teams and personal infrastructure:

  • one Node.js 22 process,
  • SQLite persistence,
  • Docker Compose deployment,
  • Nginx reverse proxy,
  • built-in backups and offline restore,
  • no external database, queue, or cache required.

Thank you in advance for your interest in the project and for every installation, test, bug report, and piece of feedback. Early real-world testing is especially valuable and will directly help shape BetterStatusPage beyond the MVP.

Before exposing a production instance, read:


What it does 🚀

🔍 Five ways to monitor things

Type What it checks
HTTPS URLs, status codes, response times, keywords in the body, and full auth flows (Basic, OAuth2, CAS)
Ping / TCP Whether a host is alive — via ICMP or TCP port check
DNS Whether your records resolve correctly — A, AAAA, MX, CNAME, TXT, with custom resolver support
SQL Server Runs a test query against MSSQL and validates the result
Webhook (passive) Lets external services ping you to signal they're alive — silence means trouble

Every monitor gets: configurable intervals, timeouts, retries, color-coded tags for grouping, and a 30-day uptime history. Oh, and there's a built-in test runner so you can validate your config before hitting Save and immediately regretting it.

🔔 Notifications that actually reach people

When something breaks (or recovers), BetterStatusPage can shout at you via:

  • Email — SMTP with TLS, custom sender, and template variables
  • Webhook — fire at Slack, PagerDuty, or literally anything with an HTTP endpoint
  • Discord — native integration with rich embeds; color-coded by severity (red = down, orange = degraded, green = recovery), no bot token required — just a webhook URL
  • Microsoft Teams — native MessageCard integration; color-coded cards, no app installation required — just an incoming webhook URL
  • Slack — native Block Kit integration; color-coded attachments, optional <!here> / <!channel> mentions — just an incoming webhook URL

All channels support template variables like {{monitor_name}}, {{status}}, {{error_message}} etc., so your alerts can say "API Gateway is down: connection timeout" instead of "status changed".

Recovery notifications are optional per channel — because sometimes you want to know when things come back up, and sometimes you just want to sleep.

Every delivery is persisted with its individual attempts. Failed sends retry automatically after 1 and 5 minutes, then remain visible in the admin delivery history for manual retry. Delivery history is retained for 180 days.

See docs/discord-integration.md for a step-by-step Discord setup guide. See docs/teams-integration.md for a step-by-step Microsoft Teams setup guide. See docs/slack-integration.md for a step-by-step Slack setup guide.

🔐 A vault for your secrets

Storing passwords in environment variables is fine until it isn't. BetterStatusPage has a built-in AES-256-GCM encrypted vault where you can stash credentials and reference them from monitors and SMTP settings. Supports:

  • userpass — classic username + password
  • value — a single secret string (API token, connection string)
  • json — a full JSON object, with field mapping to pick out exactly the keys you need

Your secrets never appear in logs, API responses, or your git diff.

🎨 A drag-and-drop page builder

The public status page isn't just a list of green dots. It's a fully customizable grid layout you design yourself — drag in monitor cards, group them by service, add markdown text blocks, drop in an incident feed, resize everything, done. No CSS required.

📢 Incident management

Create incidents, set severity (none → minor → major → critical), link affected monitors, post real-time updates as the situation unfolds, and mark resolved when the dust settles. On the public page, an active minor incident marks its linked monitors as degraded, while major and critical incidents mark them as down. Resolving the incident restores the status reported by monitoring checks.

🔧 Maintenance windows

Scheduled that 3 AM database migration? Let people know in advance instead of letting them think you're on fire.

Maintenance windows suppress alert noise for the duration of planned downtime — no more on-call pages for work you're doing on purpose. Create a window with a name, description, start/end time, and optionally scope it to specific monitors (or flip the switch for "all monitors"). While a window is active:

  • Notification channels stay quiet for affected monitors — status changes are still tracked, just not shouted
  • The public status page shows an amber banner with the window name and end time
  • Individual monitor cards display a MAINTENANCE chip so visitors know what's happening
  • The admin dashboard sorts windows into Active / Upcoming / Past so you always know what's scheduled

🕵️ Audit log

Six months from now, someone will ask "who changed the check interval on the payments monitor from 30 seconds to 5 minutes last Thursday?" The answer is in the audit log.

Every mutation in the admin panel is recorded — who did it, when, and exactly what changed. For updates, you get a field-level diff with before and after values (sensitive fields like passwords are always redacted). Covered entities:

What Tracked operations
Monitors Create, update (name, type, interval, timeout, retries, tags, config), delete
Incidents Create, update (title, status, impact), delete
Maintenance windows Create, update, delete
Notification channels Create, update (name, type, enabled, config), delete
SMTP settings Configure / update
Vaults & secrets Create, update (name, value change flagged as [redacted]), delete
Users Create, role change, password reset, delete
Account security Enable or disable TOTP two-factor authentication

The audit log page (admin-only) lets you filter by user, entity type, action (create / update / delete), and date range. Click any row to expand the diff inline.

🌍 i18n, branding, the works

  • Multi-language support — add your own locale, translate every string, set a default
  • Full branding — site name, logo, favicon, 13 theme colors, and a custom CSS field for when you really want to go off
  • Dark mode — because it's not optional anymore

🔗 Monitor dependencies

Not every outage is what it looks like. When your API goes down because the database went down, you don't want three alerts — you want one, for the actual root cause.

BetterStatusPage lets you declare that monitor A depends on monitor B. When B goes down, A automatically shows Dependency Issue instead of Down — and its alert is suppressed, because B already fired it. Once B recovers, A returns to its normal status on the next check.

On the public status page, affected monitors display a "Caused by: Database" chip so visitors understand the hierarchy at a glance, not just a wall of red dots.

Configure it per monitor in the admin panel → monitor edit → Depends on tab.

⚡ Real-time, no refresh needed

Status changes propagate to both the admin dashboard and the public page instantly via Server-Sent Events. The moment a monitor flips from 🟢 to 🔴, everyone sees it. No polling, no page refresh, no "wait, is this stale?"


Architecture 🏗️

┌─────────────────────────────────────────────────────────────┐
│                        Browser                              │
│                                                             │
│   ┌──────────────────┐        ┌──────────────────────────┐  │
│   │   Admin UI       │        │   Public Status Page     │  │
│   │   React 19       │        │   React 19               │  │
│   │   Vite · RQ · DnD│        │   SSE · i18n · dark mode │  │
│   └────────┬─────────┘        └────────────┬─────────────┘  │
└────────────┼──────────────────────────────┼────────────────┘
             │  REST + SSE                   │  REST + SSE
┌────────────▼──────────────────────────────▼────────────────┐
│                    Fastify 5  (Node.js 22+)                 │
│                                                             │
│   ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐  │
│   │  Admin API   │  │  Public API  │  │  Webhook API     │  │
│   │ Session+RBAC │  │  open        │  │  token auth      │  │
│   └──────────────┘  └──────────────┘  └──────────────────┘  │
│                                                             │
│   ┌──────────────────────────────────────────────────────┐  │
│   │               Background Workers                     │  │
│   │                                                      │  │
│   │  Scheduler                  Notifier
│   │  ├── HTTPS checker          Vault resolver           │  │
│   │  ├── Ping / TCP             Result purger (daily)    │  │
│   │  ├── DNS resolver                                    │  │
│   │  └── SQL Server checker                              │  │
│   └──────────────────────────────────────────────────────┘  │
│                                                             │
│   ┌──────────────────────────────────────────────────────┐  │
│   │           Drizzle ORM  ·  SQLite (WAL mode)          │  │
│   └──────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

Why these choices?

SQLite instead of Postgres — A status page for most teams doesn't need a database server. SQLite in WAL mode handles concurrent reads and writes without breaking a sweat, has zero ops overhead, and your entire database is a single file you can back up with cp. If your status page grows to the point where SQLite is a bottleneck, you have much bigger problems (and probably a dedicated ops team).

SSE instead of WebSockets — Status updates only ever flow server → client. SSE handles that perfectly, uses plain HTTP, works through proxies without config changes, and auto-reconnects. Why bring a sledgehammer to a nail job?

Monorepo with @bsp/shared — The API and both frontends share one TypeScript package for all domain types. Change a type in one place, the compiler yells at you everywhere it matters. No "oh we forgot to update the frontend types" post-mortems.

AES-256-GCM vault in-process — A random 12-byte IV per secret, authentication tag verification on decrypt, secrets never logged. Not a replacement for HashiCorp Vault on a 500-person engineering team, but everything you need for teams who want encrypted secrets without standing up another service.


Tech stack 🛠️

Layer Technology Version
Runtime Node.js 22+
Language TypeScript 5.7
Backend Fastify 5.x
Database SQLite (node:sqlite) built-in
ORM Drizzle 0.40
Frontend React 19
Build Vite 6.x
Styling Tailwind CSS 3.x
Data fetching TanStack Query 5.x
State Zustand 5.x
Drag & drop dnd-kit 6.x
Auth Server-side sessions, HttpOnly JWT cookies, CSRF, TOTP, bcrypt
Email Nodemailer 8.x
SQL Server mssql 11.x
Scheduler node-cron 3.x

Quick production start

Docker Compose is the recommended deployment path:

git clone https://github.com/BElluu/BetterStatusPage.git
cd BetterStatusPage
cp .env.example .env

Edit .env and set at least:

NODE_ENV=production
JWT_SECRET=<random 32+ char secret>
VAULT_ENCRYPTION_KEY=<64-char hex key>

Generate safe values:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Start the application:

export BSP_IMAGE=ghcr.io/belluu/better-status-page:0.1.3
docker compose up -d

Open http://your-server:3000/admin for first-run setup. For internet-facing deployments, put Nginx/SSL in front and keep the application port private. See docs/deployment.md.


Getting started for development ⚡

You'll need

  • Node.js 22.14+ — we use the built-in node:sqlite module, so no ancient runtimes
  • npm 10+

Installation

git clone https://github.com/BElluu/BetterStatusPage.git
cd BetterStatusPage
npm install

Development

npm run dev

Three things start:

App URL
API http://localhost:3000
Admin http://localhost:5173
Status page http://localhost:5174

Open the admin URL and you'll be greeted by a setup wizard. Create your admin account, and you're in.

Quality checks

npm run lint            # ESLint for API, shared types, and both React apps
npm test                # API integration tests + frontend component tests
npm run test:coverage   # frontend coverage report with enforced thresholds
npm run build           # strict TypeScript and production builds for all workspaces
npm run backup          # create a consistent database + uploads backup (after build)

Browser smoke tests use Playwright. Install Chromium once, then run the suite:

npx playwright install chromium
npm run test:e2e

The E2E suite stores runtime data under .e2e/. GitHub Actions runs lint, tests, coverage, builds, Playwright E2E, and a production Docker smoke test on pull requests and pushes to main.

Production

npm run build   # builds the API and both frontends
npm start       # starts the API, which serves them as static files

One process. One port. That's it.


Configuration ⚙️

Copy .env.example to .env:

PORT=3000
NODE_ENV=production
BSP_IMAGE=ghcr.io/belluu/better-status-page:0.1.3
BSP_BIND_ADDRESS=127.0.0.1

JWT_SECRET=something-long-random-and-secret
VAULT_ENCRYPTION_KEY=64-char-hex-string   # see below

DATABASE_PATH=./data/db.sqlite
UPLOAD_DIR=./data/uploads

# Comma-separated list of allowed origins for CORS
ALLOWED_ORIGINS=https://status.example.com,https://admin.example.com

# Set only when port 3000 is reachable exclusively through one trusted reverse proxy
TRUST_PROXY=1

# Optional scheduler tuning
SCHEDULER_TICK_SECONDS=10
MONITOR_CHECK_CONCURRENCY=20
MONITOR_RESULT_RETENTION_DAYS=90
MONITOR_RESULT_PURGE_CRON=0 2 * * *

🔑 Generate a vault encryption key:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Keep this somewhere safe. If it changes, all stored secrets become unreadable. Yes, all of them.


User roles 👥

Role What they can do
admin Everything, including users, vaults, audit log, and backups
operator Monitors, incidents, maintenance, notifications, page builder, branding, localization, and settings
branding Page builder, branding, localization, and account settings

Administrator sessions are stored server-side and authenticated with an HttpOnly, SameSite=Strict cookie. State-changing browser requests require a matching CSRF token. Users can enable TOTP two-factor authentication from Settings and receive eight single-use recovery codes.


Deployment 📦

Behind Nginx (recommended)

server {
    listen 443 ssl;
    server_name status.example.com;

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # SSE requires these — don't skip them
        proxy_buffering off;
        proxy_cache off;
        proxy_read_timeout 3600s;
    }
}

With PM2

npm i -g pm2
pm2 start npm --name "bsp" -- start
pm2 save && pm2 startup

With Docker Compose (recommended)

cp .env.example .env   # fill in JWT_SECRET and VAULT_ENCRYPTION_KEY
export BSP_IMAGE=ghcr.io/belluu/better-status-page:0.1.3
docker compose up -d

Data (SQLite database + uploads) is stored in a named Docker volume (bsp_data) and survives container rebuilds, restarts, and image upgrades. It is only deleted if you explicitly run docker compose down -v.

For local image development, use the local override so the production Compose file continues to use GHCR:

docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build

See docs/deployment.md for the full deployment guide, including Nginx + SSL setup, bare-metal install, PM2, backups, and firewall configuration.


Production checklist

Before making an instance public:

  • Set NODE_ENV=production.
  • Replace JWT_SECRET with a random non-default value.
  • Set VAULT_ENCRYPTION_KEY to a random 64-character hex value and store it outside the app.
  • Put the app behind Nginx/SSL and avoid exposing port 3000 directly.
  • Set TRUST_PROXY=1 only when the app is reachable exclusively through your reverse proxy.
  • Create a backup and verify it.
  • Perform a test restore on a non-production copy.
  • Confirm release checks are green: lint, tests, build, Docker build, and E2E on Linux CI.
  • Review SECURITY.md before opening public issue reporting.

Project structure 📁

BetterStatusPage/
├── apps/
│   ├── api/           # Fastify backend
│   │   └── src/
│   │       ├── db/        # Schema, migrations, seed
│   │       ├── routes/    # Endpoint handlers
│   │       ├── workers/   # Scheduler, checkers, notifier
│   │       ├── crypto/    # AES-256-GCM vault encryption
│   │       └── services/  # SSE broadcaster
│   ├── admin/         # Admin dashboard (React)
│   └── status/        # Public status page (React)
└── packages/
    └── shared/        # Shared TypeScript types (@bsp/shared)

Roadmap 🗺️

BetterStatusPage works great as a single SQLite-backed process — but we know that's not everyone's story. Here's where we're headed:

🗄️ More database backends

SQLite is perfect for getting started, but if you're running BetterStatusPage as part of a larger infrastructure where your data already lives in a managed database, you shouldn't have to compromise. We're adding native support for:

  • PostgreSQL — for teams already running Postgres, or anyone who wants point-in-time recovery, read replicas, and proper concurrent writes
  • MariaDB / MySQL — same idea, different flavor

The goal is a single DATABASE_URL config switch. No code changes, no data migration headaches — just point it at your existing database and go.

🔐 Azure Key Vault integration

The built-in vault is great for self-contained deployments, but enterprises already have their secrets somewhere else — usually Azure Key Vault. Instead of duplicating credentials, we want BetterStatusPage to pull them directly from AKV at runtime. Concretely:

  • Authenticate via managed identity or service principal
  • Reference secrets by name from Azure Key Vault in monitor and SMTP configs
  • Zero secrets stored locally — the application is just a consumer

If you're on AWS or GCP, stay tuned — this naturally extends to AWS Secrets Manager and GCP Secret Manager down the line.

🔔 More notification channels

Email, webhook, Discord, Teams, and Slack cover the most common cases, but alerting is only as good as the channels people actually watch. Still on the list:

  • Telegram — for the teams that live there
  • SMS — via Twilio or similar, for when the internet itself is on fire and nobody's checking Slack
  • PagerDuty / OpsGenie — for when "someone should look at this" needs to become "wake someone up right now"

📡 More monitor types

Five monitor types cover most cases, but there's always more ground to cover:

  • gRPC — health check support for services that don't speak HTTP
  • Redis / ValkeyPING and key presence checks for your cache layer
  • Playwright / Puppeteer — full browser-based synthetic monitoring for flows that require JavaScript rendering (login flows, checkout funnels, SPAs)
  • TLS/SSL certificate expiry — catch expired certs before your users do
  • Kafka / RabbitMQ — broker connectivity and lag monitoring
  • Custom scripted checks — run an arbitrary Node.js snippet, return a status — full flexibility for anything that doesn't fit a predefined type

💡 Have a feature request that's not on this list? Open an issue — the best roadmap items come from people actually running the thing in production.


Contributing 🤝

Found a bug? Have an idea? PRs are welcome — just open an issue first for anything bigger than a typo fix so we can discuss the approach.

  1. Fork
  2. git checkout -b feature/your-idea
  3. Build something cool
  4. Run npm run lint && npm test && npm run build
  5. Open a PR

License

MIT — do whatever you want, just don't blame us if your monitors miss a 3 AM outage because you forgot to set JWT_SECRET. 😅


☕ Support

BetterStatusPage is completely free and open source. If you find it useful and want to support continued development, voluntary donations are appreciated but never required!

Buy Me A Coffee

Your support helps keep the project alive and motivates continued development — but using BetterStatusPage, testing it, and sharing feedback is support enough!


Built with ☕, mild sleep deprivation, and a genuine hatred of status pages that lie.

About

A self-hosted status page that doesn't make you cry.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages