Live demo → (free-tier host, spins down after 15 min idle — first load may take ~30-50s to wake up)
A self-hostable uptime monitoring platform in the spirit of BetterStack / Checkly: create HTTP monitors, watch latency live over WebSockets, get incidents opened and resolved automatically, and publish a public status page — all from one Node.js codebase.
Built to production standards: worker-thread check engine, atomic multi-instance scheduling, MongoDB aggregation analytics, JWT + API-key auth, REST and GraphQL, a fully tested API, Docker deployment, and CI.
# zero configuration needed — uses an in-memory MongoDB in dev
cd server && npm install && npm run dev
# in another terminal
cd client && npm install && npm run dev
# open http://localhost:5173, sign up, add https://example.com — watch it go liveProduction (real MongoDB, single container serving API + dashboard):
cp .env.example .env # set JWT_SECRET
docker compose up --build
# open http://localhost:8000- Monitors — HTTP(S) checks with configurable interval (10s–1h), timeout, expected status, and response-keyword assertion.
- Live dashboard — every check result is pushed to the browser over Socket.io (JWT-authenticated handshake, one private room per user). No refresh, ever.
- Incidents — opened automatically after N consecutive failures, resolved on recovery, with duration tracking for MTTR reporting. Toast notifications in real time.
- Analytics — uptime %, average and p95 latency, and time-bucketed latency series computed in MongoDB aggregation pipelines (not in JS).
- Public status pages —
/status/:slug, unauthenticated, status.stripe.com-style. - Two APIs — versioned REST (
/api/v1) and GraphQL (/api/graphql) over the same auth and data layer. Programmatic access viaX-Api-Key.
flowchart LR
subgraph Browser
UI[React dashboard]
end
subgraph Node.js — clusterable
API[Express REST v1 + GraphQL]
WS[Socket.io<br/>JWT handshake, room per user]
SCHED[Scheduler<br/>atomic claim via findOneAndUpdate]
POOL[worker_threads pool<br/>HTTP checks off the event loop]
INC[Incident engine<br/>threshold open / auto resolve]
end
DB[(MongoDB<br/>TTL-expired checks, aggregations)]
TARGETS[[Your endpoints]]
UI <-->|REST / GraphQL| API
UI <-->|live check results| WS
SCHED -->|claims due monitors| DB
SCHED --> POOL --> TARGETS
POOL --> SCHED
SCHED --> INC --> WS
SCHED -->|check results| DB
API --> DB
- Checks never block the API. HTTP probes run on a fixed-size
worker_threadspool. A burst of slow or hung endpoints saturates the pool queue, not the event loop serving requests. - Horizontally scalable scheduling. Due monitors are claimed with an atomic
findOneAndUpdatethat pushesnextRunAtforward using an aggregation-pipeline update. Run 1 process or 10 (CLUSTER=trueforks per core) — each check still executes exactly once. - High-volume data is bounded. Raw check documents carry a 30-day TTL index; dashboards read denormalised live state on the monitor document, so listing 100 monitors costs one query, not a join.
- Analytics belong in the database. Uptime %, p95, and time-bucketed series are single aggregation pipelines — the Node process never loads raw check history into memory.
- Security defaults on. Helmet, per-route and global rate limits (tighter on auth to slow credential stuffing), bcrypt password hashing, JWT expiry, request-body size limits, per-user data isolation enforced in every query, and API keys that are hashed-key-shaped secrets rotated server-side.
- Zero-config developer experience. No
MONGO_URLin dev? The server bootsmongodb-memory-serverautomatically. Clone →npm run dev→ working product.
POST /api/v1/auth/register {email, name, password} → {user, token}
POST /api/v1/auth/login {email, password} → {user, token}
GET /api/v1/auth/me
POST /api/v1/auth/api-key rotate programmatic API key
GET /api/v1/monitors ?page&limit&status&q (paginated)
POST /api/v1/monitors {name, url, intervalSeconds, expectedStatus, keyword?}
GET /api/v1/monitors/:id
PATCH /api/v1/monitors/:id
DELETE /api/v1/monitors/:id
GET /api/v1/monitors/:id/checks recent raw checks
GET /api/v1/monitors/:id/stats ?range=1h|24h|7d → summary + chart series
GET /api/v1/incidents ?status&monitor (paginated)
GET /api/v1/status/:slug public status page (no auth)
POST /api/graphql e.g. { monitors { name status summary { uptimePct p95Latency } } }
WebSocket events (Socket.io, auth: { token }): check:result, incident:opened, incident:resolved.
19 integration tests (Jest + Supertest + in-memory MongoDB) cover auth, ownership isolation, monitor CRUD and validation, the full incident lifecycle against a real local HTTP target that is toggled healthy/unhealthy mid-test, stats aggregations, GraphQL, and public status pages.
cd server && npm testCI (GitHub Actions) runs the suite on Node 20 and 22, builds the dashboard, and builds the production Docker image on every push.
CLUSTER=true npm startforks one worker per core; the atomic scheduler claim makes this safe.- Stateless API (JWT) → drop it behind any load balancer.
- To go multi-node with WebSockets, add the Socket.io Redis adapter — the emit layer is isolated in
src/sockets/io.jsfor exactly that reason.
Node.js · Express · MongoDB/Mongoose · Socket.io · worker_threads · GraphQL · JWT · Jest/Supertest · React (Vite) · Docker · GitHub Actions
- Create a free MongoDB Atlas M0 cluster, add a database user, and allow network access from
0.0.0.0/0(or Render's static IPs on paid plans). Copy the connection string. - Click Deploy to Render above (or New + → Blueprint, connect this repo — Render reads
render.yamlautomatically). - Paste the Atlas connection string into the
MONGO_URLenvironment variable when prompted.JWT_SECRETis generated for you. - Render builds the
Dockerfile(client build → server) and deploys a single web service exposing the API, GraphQL endpoint, WebSocket server, and the React dashboard.