PulseWatch is a full-stack monitoring platform for developers who want to know when their services break — before users notice. It probes your endpoints every minute, tracks uptime history, opens incidents automatically on repeated failures, sends alerts to Telegram / Email / Discord / Slack / Webhooks, and gives you a public status page to share with your users. AI-powered incident explanations tell you why something broke in plain English.
| 🔔 Instant Alerts | 📊 Live Dashboard | 🌐 Public Status | 🤖 AI Explanations |
|---|---|---|---|
| Telegram, Email, Discord, Slack, Webhooks | Real-time KPIs, uptime %, response time | Shareable status board with 3 themes | OpenRouter-powered plain-English root causes |
| Dashboard → | Status Page → | Alerts → | AI → |
# Clone the repo
git clone https://github.com/Robibiruk/PulseWatch.git
cd PulseWatch
# Python backend
cd backend
python -m venv .venv
.venv/Scripts/activate # macOS/Linux: source .venv/bin/activate
pip install -r requirements.txt
# Create .env (or export vars)
cp .env.example .env
# Edit .env — set DATABASE_URL and SECRET_KEY at minimum
uvicorn main:app --reload --port 8000The API runs at http://localhost:8000 — interactive docs at /docs.
cd frontend
npm install
npm run devOpen http://localhost:5173, register an account, add your first monitor.
# One-command local startup (both services)
./start-all.bat # Windows
npm run dev:all # cross-platformSee Production Deployment for the full guide (Render + Vercel + Neon + GitHub Actions).
Login JWT auth + GitHub OAuth |
Dashboard Real-time KPIs + monitor fleet |
Public Status Page Auth-free shareable board with 3 themes (neon / light / minimal) |
|
| Category | What you get |
|---|---|
| Monitors | HTTP/HTTPS, heartbeat (push), configurable interval (1–30 min), SSL checks, domain expiry |
| Alerts | Telegram, Email (Resend), Discord webhook, Slack webhook, generic JSON webhook |
| Dashboard | Real-time KPIs, uptime %, response time, monitor fleet with filters |
| Incidents | Auto open/resolve, duration tracking, severity, AI root-cause explanations |
| Status Pages | 3 themes (neon / light / minimal), auth-free, shareable URLs |
| Bot | Telegram long-polling: /status, /incidents, /pause, /resume |
| Auth | Email/password + JWT, bcrypt, API tokens |
| Config | Per-monitor: timeout, redirects, IP version, auth (basic/bearer), status codes |
Dev (single process): The FastAPI lifespan starts the worker loop and Telegram bot in-process.
graph TD
A[React SPA<br/>Vite + TS] -->|REST + JWT| B[FastAPI App<br/>main:app]
B -->|lifespan| C[Worker Loop<br/>scheduler]
B -->|lifespan| D[Telegram Bot<br/>long-poll]
C --> E[check_site]
E --> F[(Incidents + Alerts)]
D -->|/start token| G[/auth/telegram/connect/]
F --> H[Notifications<br/>Telegram / Email / Discord / Slack / Webhook]
Prod (recommended): separate services. Render runs the API + worker + bot against one Postgres. The worker uses SELECT FOR UPDATE SKIP LOCKED so 2+ replicas safely share monitors with zero duplicate checks or alerts.
graph TD
subgraph Services
API[pulsewatch-api]
Worker[pulsewatch-worker]
Bot[pulsewatch-bot]
end
DB[(PostgreSQL / Neon)]
API --> DB
Worker -->|claim lock + SKIP LOCKED| DB
Bot --> DB
API -->|serves dashboard +<br/>public status pages| Users((Users))
Worker --> Notify[Notification Dispatcher]
Claim-lock state machine: the scheduler atomically claims each due monitor before probing — if a worker crashes, its lease expires and another picks up. No duplicates, no false alerts.
PulseWatch is designed for free-tier hosting: Neon (Postgres) + Render (API) + Vercel (frontend) + GitHub Actions (always-on worker cron).
graph LR
subgraph Free Tier
FE[Vercel<br/>React SPA]
API[Render<br/>FastAPI + Worker]
GH[GitHub Actions<br/>5-min cron]
DB[(Neon<br/>Postgres)]
end
FE -->|CORS| API
API --> DB
GH -->|every 5 min| DB
FE -->|VITE_API_BASE| API
| Service | Host | What to set |
|---|---|---|
| Database | Neon | Create project → copy pooled connection string as DATABASE_URL |
| API | Render | Web Service → root backend → start: bash start.sh |
| Frontend | Vercel | Project → root frontend → env: VITE_API_BASE=<render URL> |
| Docs | Vercel | Project → root docs-site (Docusaurus) |
| Worker cron | GitHub Actions | monitor.yml runs every 5 min → secrets: DATABASE_URL, SECRET_KEY |
Critical env vars on Render:
CORS_ORIGINS→ your Vercel frontend URL (comma-separated)PUBLIC_BASE_URL→ your Render API URL (for Telegram deep links)SECRET_KEY→ long random string
| Layer | Technology |
|---|---|
| Backend | Python 3.12, FastAPI, SQLAlchemy 2.0 (async), Pydantic v2 |
| Frontend | React 18, Vite, TypeScript, React Router v6 |
| Database | PostgreSQL (Neon) or SQLite (dev) |
| Auth | JWT (python-jose), bcrypt, API tokens |
| Notifications | Telegram Bot API, Resend (email), Discord/Slack webhooks |
| AI | OpenRouter (GPT-OSS-20b:free) for incident explanations |
| Deployment | Render (API), Vercel (frontend), GitHub Actions (worker cron) |
PulseWatch/
├── backend/
│ ├── main.py # FastAPI app + lifespan (worker + bot)
│ ├── worker.py # Scheduler — claim-lock + SKIP LOCKED
│ ├── checker.py # HTTP probe, SSL, domain checks
│ ├── telegram_bot.py # Long-polling bot + account linking
│ ├── notifications.py # Multi-channel alert dispatch
│ ├── emailer.py # Branded HTML email templates
│ ├── ai_explain.py # OpenRouter AI incident explanations
│ ├── models.py # SQLAlchemy 2.0 async ORM
│ ├── schemas.py # Pydantic request/response schemas
│ ├── database.py # Engine + additive column migrations
│ ├── config.py # pydantic-settings config
│ ├── requirements.txt # Python dependencies
│ ├── render.yaml # Render deployment config
│ └── routers/ # auth, monitors, status, telegram,
│ # heartbeat, statuspage, notifications, platform
├── frontend/
│ ├── src/
│ │ ├── App.tsx # React Router routes
│ │ ├── api.ts # API client (fetch + JWT)
│ │ ├── auth.tsx # Auth context (login/register/logout)
│ │ ├── components/ # Layout, Icon, MonitorEditor, Wizard
│ │ └── pages/ # Landing, Dashboard, Settings, Profile,
│ │ # MonitorDetail, Incidents, PublicStatus
│ └── vite.config.ts
├── docs-site/ # Docusaurus documentation site
│ ├── docs/ # MDX documentation pages
│ └── blog/ # Changelog + technical posts
└── README.md
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
(required) | SQLAlchemy async URL — postgresql+asyncpg://... or sqlite+aiosqlite:///./pulsewatch.db |
SECRET_KEY |
dev-insecure-change-me |
JWT signing secret — set a long random value in production |
CORS_ORIGINS |
http://localhost:5173 |
Comma-separated allowed browser origins |
| Variable | Default | Description |
|---|---|---|
POLL_INTERVAL |
15 |
Base scheduler tick (seconds) |
WORKER_CONCURRENCY |
20 |
Max concurrent checks |
NO_WORKER |
false |
Set true to disable the built-in scheduler (when GitHub Actions owns checks) |
FAILURE_THRESHOLD |
3 |
Consecutive failures before incident opens |
CONFIRMATION_DELAY |
10 |
Seconds to confirm recovery |
| Variable | Default | Description |
|---|---|---|
TELEGRAM_BOT_TOKEN |
"" |
Bot token from @BotFather — enables bot + Telegram alerts |
NO_TELEGRAM_BOT |
false |
Set true to disable the bot poller |
| Variable | Default | Description |
|---|---|---|
RESEND_API_KEY |
"" |
Resend API key |
ALERT_FROM_EMAIL |
alerts@yourdomain.com |
From address (must be verified in Resend) |
| Variable | Default | Description |
|---|---|---|
OPENROUTER_API_KEY |
"" |
OpenRouter key for AI incident explanations |
OPENROUTER_MODEL |
openai/gpt-oss-20b:free |
Model used |
| Variable | Default | Description |
|---|---|---|
VITE_API_BASE |
http://localhost:8000 |
Backend base URL the SPA calls |
Full
.env.exampleatbackend/.env.example
All authenticated routes require Authorization: Bearer <jwt>.
| Method | Path | Description |
|---|---|---|
POST |
/auth/register |
Register account |
POST |
/auth/token |
Login → {access_token} |
GET |
/auth/me |
Current user |
GET |
/monitors |
List monitors |
POST |
/monitors |
Create monitor |
PATCH |
/monitors/:id |
Update monitor |
DELETE |
/monitors/:id |
Delete monitor |
GET |
/monitors/summary |
Fleet KPIs |
GET |
/monitors/incidents |
Recent incidents |
GET |
/status/:userId |
Public status board (no auth) |
POST |
/api/heartbeat/:token |
Push heartbeat ping |
GET |
/status/health |
Public health check |
POST |
/api/platform/tokens |
Create API token |
POST |
/api/platform/account/password |
Change password |
Interactive Swagger docs at
http://localhost:8000/docswhen the backend is running.
The bot (telegram_bot.py) runs via long-polling — no webhook setup needed. It clears any leftover webhook on startup and responds to:
| Command | What it does |
|---|---|
/start <token> |
Links your Telegram chat to your PulseWatch account |
/status |
Shows your monitors with up/down state |
/monitors |
Lists monitors with URL + check interval |
/incidents |
Recent outages and recoveries |
/pause |
Pauses all alerts |
/resume |
Resumes alerts |
/help |
Lists commands |
Linking flow: Dashboard → Settings → Connect Telegram → opens t.me/<BotUsername>?start=<token> → press Start in Telegram → account linked.
When a monitor goes down (or recovers), PulseWatch dispatches to every enabled channel for that user:
- Telegram — to your linked chat (instant)
- Email — branded HTML via Resend (incident + resolution + signup + checkin digests)
- Discord — JSON webhook with formatted message
- Slack — incoming webhook
- Generic webhook — JSON envelope (Zapier/Make/custom)
Channel enablement is per-user (Settings → Alert Channels). Users can pause all alerts globally.
The core monitor. Configurable: interval, timeout, redirects, IP version (auto/IPv4/IPv6), HTTP method (GET/HEAD/POST), auth (basic/bearer), status-code buckets (2xx/3xx), SSL checks, domain expiry reminders.
Your service pings a unique token URL (POST /api/heartbeat/:token) on a schedule. If no ping arrives within the interval, PulseWatch opens a "down" incident. Perfect for cron jobs, workers, and background tasks with no public HTTP endpoint.
- Set
SECRET_KEYto a long random value (generate:python -c "import secrets; print(secrets.token_urlsafe(48))") - Set
CORS_ORIGINSto your real frontend domain(s) — never*in production - Use Postgres (Neon) in production — SQLite is dev-only
- Rotate
TELEGRAM_BOT_TOKENandRESEND_API_KEYvia secret store, not committed files - Put the API behind a reverse proxy with rate limiting for
/auth/tokenand/api/heartbeat/:token
- Alembic migrations — replace additive column migrations
- Durable job queue (Redis/RQ or ARQ) — survive worker restarts, scale past ~1k monitors
- Second-region confirmation — re-check down signals before alerting
- Rate limiting on auth and heartbeat endpoints
- Incident acknowledgement + assignment
- Maintenance windows — scheduled alert pauses
- Escalation policies — notify a second channel after N minutes unacked
- Monitor tags / groups
- 30–90 day SLA charts
- Docker / docker-compose one-command self-host
- Team accounts + RBAC
- MVP: ≤100 monitors, 1 worker, 1-min checks
- Beta: ~1k monitors, Redis queue, multiple worker replicas (claim-lock safe)
- Production: 10k+ monitors, distributed workers across regions


