Skip to content

Aditya07771/StatusPing

Repository files navigation

StatusPing

Self-hosted uptime monitoring and public status page platform

Built with Node.js Β· BullMQ Β· PostgreSQL Β· Redis Β· Next.js 15

CI Coverage Lighthouse Deploy

Live Demo Β· Dashboard Β· API Docs


What It Is

StatusPing continuously checks whether your web services are reachable, measures their response times, automatically opens and resolves incidents, notifies you via email or webhook, and serves a public status page your users can check β€” no authentication required.

Think of it as your own self-hosted UptimeRobot. No vendor lock-in, no monthly fees, full control over every layer.


Architecture

StatusPing Architecture Diagram

The architecture below illustrates how StatusPing monitors services, detects outages, manages incidents, and delivers notifications using asynchronous background workers powered by BullMQ.

Monitoring & Incident Processing Flow

graph TD

    subgraph "Client Application"
        User["πŸ‘€ User"]
        Dashboard["Dashboard & API<br/>Next.js β€’ Auth.js β€’ REST API"]
    end

    subgraph "Core Infrastructure"
        Postgres[("PostgreSQL<br/>Users β€’ Monitors β€’ Ping Logs β€’ Incidents")]
        Redis[("Redis<br/>Queues β€’ Cache β€’ Rate Limits")]
        Scheduler["BullMQ Scheduler<br/>Schedules Monitoring Jobs"]
    end

    subgraph "Background Processing Workers"
        Ping["Ping Worker<br/>Performs HTTP/HTTPS Health Checks"]
        Incident["Incident Engine<br/>Detects Failures & Manages Incidents"]
        Notification["Notification Worker<br/>Sends Alerts & Recovery Notifications"]
    end

    subgraph "External Services"
        Service["Monitored Services<br/>Websites & APIs"]
        Email["Email & Webhook Providers"]
        Status["Public Status Page"]
    end

    User --> Dashboard

    Dashboard --> Postgres
    Dashboard --> Redis
    Dashboard --> Scheduler

    Scheduler --> Ping
    Scheduler --> Incident
    Scheduler --> Notification

    Ping -->|"HTTP Health Checks"| Service
    Ping -->|"Store Ping Results"| Postgres

    Incident -->|"Open / Resolve Incidents"| Postgres

    Notification -->|"Email & Webhook Alerts"| Email
    Notification -->|"Cooldown Tracking"| Redis

    Postgres -->|"Live Status & History"| Status
Loading

Key Features

Monitoring

  • HTTP/HTTPS health checks at 1, 5, 15, 30, or 60-minute intervals
  • Configurable failure threshold (default: 2 consecutive failures before incident)
  • Keyword assertion in response body
  • Redirect tracking β€” flags unexpected destination changes
  • SSL certificate expiry alerts (30-day warning)

Incident Engine

  • Auto-open incidents after N consecutive failures
  • Auto-resolve when the monitor recovers
  • PostgreSQL partial unique index prevents duplicate incidents under concurrent workers
  • Incident timeline with duration, MTTD, and MTTR computed automatically

Notifications

  • Email via Resend on incident open and resolve
  • HMAC-SHA256-signed webhook delivery with exponential backoff (1s β†’ 2s β†’ 4s β†’ 8s β†’ 16s, max 5 attempts)
  • Notification cooldown via Redis TTL β€” no alert storms during extended outages
  • Dead-letter table for failed deliveries with manual replay support

Security

  • API rate limiting: 100 requests/minute per authenticated user via Redis sliding window counter
  • SSRF prevention: monitor URLs are resolved to an IP at creation time and rejected if they target private ranges (127.0.0.0/8, 10.0.0.0/8, 192.168.0.0/16, 169.254.0.0/16, etc.) β€” re-validated at ping execution time to catch DNS rebinding attacks
  • Monitor ownership enforced on every API handler: monitor.user_id === session.user.id β€” never fetch by ID alone
  • Webhook secrets stored encrypted (AES-256-GCM), never in plaintext

Public Status Page (/status)

  • No login required β€” share one URL with your users
  • 90-day uptime history (one colored square per day, like GitHub's status page)
  • Active incident banner with live updates
  • Server-rendered via Next.js App Router β€” Lighthouse score 97

Data & Reporting

  • Response time P50/P95/P99 per monitor per day, charted over 90 days
  • Monthly SLA PDF export: uptime %, longest incident, MTTD, MTTR
  • 30-day raw ping log retention β†’ aggregated into daily_stats indefinitely
  • Partition-based deletion (DROP TABLE ping_logs_YYYY_MM) β€” O(1) vs millions of DELETEs

Tech Stack

Layer Technology
Frontend Next.js 15 App Router, TypeScript, Tailwind CSS, shadcn/ui, Recharts
Backend API Next.js API routes, Prisma ORM, Zod validation
Background Jobs BullMQ, Redis
Database PostgreSQL (partitioned tables, partial unique indexes)
Auth Auth.js v5, GitHub OAuth
Email Resend
Testing Vitest (unit + integration), Playwright (E2E)
Deploy Railway (two services from one monorepo), GitHub Actions

Project Structure

StatusPing/
β”œβ”€β”€ backend/                   # Node.js worker process (Railway Service 2)
β”‚   β”œβ”€β”€ prisma/
β”‚   β”‚   β”œβ”€β”€ schema.prisma      # Full schema with partitioning strategy
β”‚   β”‚   └── seed.ts            # Demo data: 3 monitors, 30 days of ping history
β”‚   └── src/
β”‚       β”œβ”€β”€ api/               # Express server exposing /health endpoint
β”‚       β”œβ”€β”€ config/            # Prisma, Redis, environment config
β”‚       β”œβ”€β”€ lib/               # crypto, SSRF blocklist, uptime calc, logger
β”‚       β”œβ”€β”€ queues/            # BullMQ queue definitions and helpers
β”‚       β”œβ”€β”€ services/          # Business logic: ping, incident, notification, SSL
β”‚       └── worker/            # Queue consumers: ping, incident, notification, retention
β”‚
β”œβ”€β”€ frontend/                  # Next.js 15 app (Railway Service 1)
β”‚   └── src/
β”‚       β”œβ”€β”€ app/
β”‚       β”‚   β”œβ”€β”€ dashboard/     # Authenticated monitor management
β”‚       β”‚   β”œβ”€β”€ status/        # Public status page (no auth)
β”‚       β”‚   └── api/           # REST API route handlers
β”‚       β”œβ”€β”€ components/        # Dashboard panels, status page, UI primitives
β”‚       β”œβ”€β”€ hooks/             # useAuth, useTheme
β”‚       └── lib/               # API client, types, utils
β”‚
β”œβ”€β”€ docker-compose.yml         # Local dev: app + worker + postgres + redis
└── Dockerfile                 # Multi-stage: target=app and target=worker

Local Setup

Requirements: Docker and Docker Compose

git clone https://github.com/yourusername/statusping.git
cd statusping

cp .env.example .env
# Fill in: AUTH_SECRET, AUTH_GITHUB_ID, AUTH_GITHUB_SECRET, RESEND_API_KEY

docker compose up --build

The seed script runs automatically and populates 3 demo monitors with 30 days of realistic ping history. Open http://localhost:3000 β€” the dashboard is ready immediately.

App:      http://localhost:3000
Status:   http://localhost:3000/status
Worker:   running as a separate container
Postgres: localhost:5432
Redis:    localhost:6379

Environment Variables

# .env.example

DATABASE_URL="postgresql://statusping:password@postgres:5432/statusping"
REDIS_URL="redis://redis:6379"

AUTH_SECRET=""                  # openssl rand -base64 32
AUTH_GITHUB_ID=""
AUTH_GITHUB_SECRET=""

RESEND_API_KEY=""
RESEND_FROM_EMAIL="alerts@yourdomain.com"

WEBHOOK_ENCRYPTION_KEY=""       # openssl rand -base64 32
NEXT_PUBLIC_APP_URL="http://localhost:3000"

PING_WORKER_CONCURRENCY="10"
NOTIFICATION_COOLDOWN_SECONDS="1800"
PING_LOG_RETENTION_DAYS="30"

Database

# Apply schema
npx prisma migrate deploy

# Seed demo data
npx prisma db seed

# Open Prisma Studio
npx prisma studio

Schema highlights:

  • ping_logs partitioned by month β€” old partitions are dropped wholesale, not row-by-row
  • Partial unique index on incidents(monitor_id) WHERE status = 'open' β€” prevents duplicate incidents at the database level under concurrent workers
  • duration_seconds on incidents is a generated column β€” computed automatically on resolve

Testing

cd backend

# Unit tests (pure functions β€” no DB, no Redis)
npm run test:unit

# Integration tests (requires running Postgres + Redis)
npm run test:integration

# Coverage report
npm run test:coverage

Unit test coverage targets:

Module Coverage
Uptime % calculation 100%
Incident trigger/resolve logic 100%
HMAC signature 100%
Notification cooldown 100%
SSRF blocklist 90%
SSL expiry evaluation 90%
cd frontend

# E2E tests (requires full stack running)
npx playwright test

API Reference

All authenticated endpoints require a valid Auth.js session. All timestamps are ISO 8601 UTC. Rate limit: 100 requests/minute per user β€” exceeded requests receive 429 with a Retry-After header.

Method Endpoint Description
GET /api/monitors List all monitors (paginated)
POST /api/monitors Create monitor
GET /api/monitors/:id Monitor detail with recent pings
PATCH /api/monitors/:id Update / pause / resume
DELETE /api/monitors/:id Soft-delete
GET /api/monitors/:id/ping-logs Ping history
GET /api/monitors/:id/incidents Incident history
GET /api/monitors/:id/response-times P50/P95/P99 chart data
POST /api/monitors/:id/notifications Add email or webhook config
GET /api/status Public status data (no auth)
GET /api/health Service health check
GET /api/monitors/:id/report/sla Download SLA PDF

Full OpenAPI spec available at /api/docs.


Deployment (Railway)

Two Railway services, one monorepo:

# Service 1 β€” Next.js app
railway up --service app

# Service 2 β€” BullMQ worker
railway up --service worker

The Dockerfile uses multi-stage builds. Both services share the same Railway PostgreSQL and Redis add-ons. On worker startup, it reads all active monitors from PostgreSQL and re-registers BullMQ repeatable jobs β€” Redis data loss does not require manual intervention.

CI/CD via GitHub Actions: push to main β†’ Vitest + Playwright β†’ Docker build β†’ Railway deploy. Failing tests block the merge.


Design Decisions

Why a separate Railway service for the worker? BullMQ requires a long-running Node.js process. Vercel/serverless functions time out at 10 seconds and cannot run persistent background jobs. Separating the worker also means a slow batch of HTTP checks never blocks dashboard API responses.

Why BullMQ over node-cron or setTimeout? BullMQ jobs are persisted in Redis. A worker crash does not lose scheduled jobs β€” BullMQ re-enqueues any job whose lock expires. node-cron jobs vanish on restart, cannot be distributed across workers, and have no retry or dead-letter capability.

Why PostgreSQL table partitioning for ping_logs? At 100 monitors Γ— 1 ping/min, logs grow by 52 million rows per year. DELETE FROM ping_logs WHERE checked_at < '30 days ago' would scan and lock millions of rows. DROP TABLE ping_logs_2024_10 is a single metadata operation β€” instant, no contention.

Why Redis for rate limiting instead of an in-process counter? An in-memory counter resets on every worker restart and cannot be shared across multiple instances of the dashboard service. Redis INCR + EXPIRE implements a sliding window that survives restarts and works correctly whether one or ten dashboard instances are running. The counter key is ratelimit:{userId}:{windowStart} with a 60-second TTL β€” no cleanup job required.

Why a partial unique index on incidents? CREATE UNIQUE INDEX ON incidents(monitor_id) WHERE status = 'open' enforces at most one open incident per monitor at the database level. Two concurrent workers detecting the same failure will race to INSERT; one succeeds, the other gets a 23505 constraint violation and skips gracefully β€” no application-level locking needed.


License

MIT

About

StatusPing is a production-inspired uptime monitoring platform that continuously monitors websites and APIs, automates incident detection, delivers real-time alerts, and powers public status pages using a scalable backend architecture

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors