100% free & open source uptime monitoring. Monitor any HTTP(S) endpoint, get public status pages with 90-day uptime bars, and receive Slack + email alerts the second something breaks.
No plans. No paywalls. No limits. Unlimited monitors, any check interval, forever — on the hosted instance or self-hosted (MIT license).
Everything runs on free tiers:
| Piece | Tech | Cost |
|---|---|---|
| App + API routes | Next.js 14 (App Router) + TypeScript + Tailwind | — |
| Database & Auth | Supabase (free tier) + Row Level Security | $0 |
| Check engine | GitHub Actions cron (any schedule your repo allows) | $0 |
| Hosting | Vercel (Hobby) | $0 |
| Alerts | Slack incoming webhooks + Resend (free tier) | $0 |
GitHub Actions (cron)
│ POST /api/cron/run-checks (header: x-cron-secret)
▼
Vercel └─ Next.js route ── service-role client ──▶ Supabase
│
probes URL, writes monitor_checks │ opens/resolves incidents
│ │ sends alerts
└──▶ Slack webhook / Resend email │
▼
/status/[username] ◀── reads via service role (no auth)
- Check worker — GitHub Actions schedules
POST /api/cron/run-checks. The route verifies thex-cron-secretheader againstCRON_SECRET, then probes every active monitor that is due. When a monitor is down and has no open incident it opens one + alerts; when it recovers it resolves the incident + sends a recovery alert. - No limits, by design — the plan-limit trigger and Stripe billing were
removed in
010_open_source_unlock.sql(and thestripepackage dropped from the codebase). Theplans/subscriptionstables remain as metadata only; nothing gates on them. Every account gets unlimited monitors. - Public status page —
/status/[username]is public. It reads data with the service-role key server-side and auto-refreshes every 30 seconds viaGET /api/status/[username]. - Free-tier note — GitHub Actions only schedules workflows every 5
minutes or coarser on its free plan. The workflow ships with
*/5 * * * *. If your repo allows a faster schedule, switch to* * * * *for every-minute checks. Monitors also gate themselves byinterval_minutes, so nothing is double-checked.
.
├── .env.example # all env vars, documented
├── .github/workflows/uptime-check.yml
├── supabase/migrations/
│ ├── 001_initial.sql # schema, RLS, triggers, uptime aggregates
│ ├── 002_saas.sql # plans, subscriptions (metadata only)
│ ├── 003_v2_phase1.sql # status_pages, subscribers, maintenance, push
│ ├── 004–009_v2_*.sql # v2 features, security hardening, premium
│ └── 010_open_source_unlock.sql # drops plan-limit trigger → 100% free
├── src/
│ ├── middleware.ts # session refresh
│ ├── app/
│ │ ├── layout.tsx / page.tsx # root layout (PWA manifest) + landing
│ │ ├── login / signup / auth/callback
│ │ ├── dashboard/
│ │ │ ├── layout.tsx # auth guard + nav
│ │ │ ├── page.tsx # monitor list + 30d stats + subscriber count
│ │ │ ├── monitors/new # add monitor form
│ │ │ ├── monitors/[id] # chart, uptime bars, incidents
│ │ │ ├── billing # open source page (no plans)
│ │ │ ├── maintenance # schedule planned downtime
│ │ │ └── settings # alert channels + push notifications
│ │ ├── status/[username] # public status page (+ subscribe form)
│ │ └── api/
│ │ ├── cron/run-checks # the check worker entrypoint
│ │ ├── cron/digest # weekly digest email (Mon 9am)
│ │ ├── status/[username] # public JSON status API
│ │ ├── status/[username]/subscribe # add a status page subscriber
│ │ ├── status/[username]/confirm # confirm a subscription
│ │ ├── push/subscribe # save a Web Push subscription
│ │ ├── push/unsubscribe # remove a Web Push subscription
│ │ ├── push/vapid-public-key# expose the VAPID public key
│ ├── components/ # UI pieces (Tailwind only)
│ └── lib/ # supabase clients, checks, alerts, subscribers, push, digest
You can run the entire SaaS locally with zero cloud sign-ups: a local
PostgreSQL (Supabase-compatible) plus a small Node shim that speaks the
/auth/v1 + /rest/v1 Supabase API against it. Auth, Row Level Security,
incidents and alerts all work exactly as they do in production — only
Slack/Resend delivery is skipped when keys are absent (each fails gracefully
and logs).
Requires: Homebrew (brew install postgresql@16 postgrest) and Node 18+.
# 1. Build the stack (initdb, migrations, secrets → .env.local)
.local/stack.sh setup
# 2. Start postgres + PostgREST + shim
.local/stack.sh start # status / stop / reset also available
# 3. Run the app (uses .env.local)
npm run dev # → http://localhost:3000What the shim provides:
| Endpoint | Purpose |
|---|---|
POST /auth/v1/signup |
real signup → auto profile + subscription |
POST /auth/v1/token |
password + refresh grants, real JWT |
GET /auth/v1/user |
session lookup used by middleware |
POST /rest/v1/* |
pass-through to PostgREST (RLS + RPCs) |
A good end-to-end demo: sign up, add a monitor pointing at
http://127.0.0.1:59999 (everything is unlocked — no plans), then:
curl -X POST http://localhost:3000/api/cron/run-checks \
-H "x-cron-secret: local-cron-secret"The dead port goes down, an incident opens and Slack/email alert code
runs. Point the monitor at a healthy URL, wait for its interval, run again —
the incident resolves. Watch it all on /status/<username> and in the
dashboard.
git clone https://github.com/you/pingbeacon.git
cd pingbeacon
npm install
cp .env.example .env.local- Create a project at https://supabase.com.
- In the SQL Editor, paste the contents of
supabase/migrations/001_initial.sqland run it. (If you have the Supabase CLI,supabase db pushalso works.) - Project Settings → Authentication → URL Configuration:
Site URL: your Vercel URL (orhttp://localhost:3000in dev)Redirect URLs: addhttp://localhost:3000/auth/callbackandhttps://<your-app>.vercel.app/auth/callback.
- Under Authentication → Providers, confirm Email is enabled.
Uptime stats and the 90-day bars are computed by SQL aggregate functions (
get_uptime_bars_many,get_monitor_stats_many) so the app never pulls thousands of raw check rows — nomax rowschange is needed. The only raw fetch is the 24h response-time chart; if you check at 1-minute intervals you may optionally raise Settings → API → Max rows per request to10000.
NEXT_PUBLIC_SUPABASE_URL= # Project Settings → API → Project URL
NEXT_PUBLIC_SUPABASE_ANON_KEY= # Project Settings → API → anon public key
SUPABASE_SERVICE_ROLE_KEY= # Project Settings → API → service_role (server only!)
CRON_SECRET= # run: openssl rand -hex 32
RESEND_API_KEY= # https://resend.com → API Keys (free)
RESEND_FROM_EMAIL= # verified sender, e.g. PingBeacon <onboarding@resend.dev>
NEXT_PUBLIC_APP_URL=http://localhost:3000Keep
SUPABASE_SERVICE_ROLE_KEYandCRON_SECRETserver-only. TheNEXT_PUBLIC_*keys are safe in the browser because Row Level Security is enabled.
npm run dev
# → http://localhost:3000Create an account (username becomes your status-page slug), add a monitor, and configure alerts in Settings.
- Push the repo to GitHub, then import it at https://vercel.com/new.
- Add all variables from
.env.localas Environment Variables (Project → Settings → Environment Variables). MarkNEXT_PUBLIC_*as such. - Deploy. Note the production URL, e.g.
https://pingbeacon-five.vercel.app.
Open your repo → Settings → Secrets and variables → Actions and add:
| Secret | Value |
|---|---|
PINGBEACON_URL |
pingbeacon-five.vercel.app (no https://) |
CRON_SECRET |
the same value you used for CRON_SECRET |
The workflow runs on schedule automatically and also has a Run workflow button for manual runs.
- Slack — create an Incoming Webhook at https://api.slack.com/apps →
your app → Incoming Webhooks → copy the
https://hooks.slack.com/services/…URL into Dashboard → Settings. - Email — create a free Resend key. To send to real addresses you must
verify a domain in Resend and set
RESEND_FROM_EMAILto an address on it; without a domain,onboarding@resend.devcan only send to your own account email. Emails are sent on incident open and on recovery.
PingBeacon has no billing. The paid plans and the Stripe integration were
removed when the project went fully open source (see
010_open_source_unlock.sql). There is nothing to configure, nothing to pay
for, and no limits to hit — on the hosted instance or self-hosted.
The V2 upgrade adds the "stickiness core" — features that keep users on the platform and make status pages self-promoting.
Every public status page has a "Get notified" form. Visitors subscribe with an email, confirm via a Resend email, and then automatically receive incident updates when a monitor goes down and when it recovers. The owner sees the confirmed subscriber count in their dashboard.
POST /api/status/[username]/subscribe— create subscriber + send confirmationGET /api/status/[username]/confirm?token=…— confirm a subscription- Emails are sent by the check worker on incident open/resolve via Resend
Schedule planned downtime from Dashboard → Maintenance. During a window:
- Checks still run and are recorded (history stays complete),
- Incident creation and all alerting (Slack, email, push, subscribers) is suppressed,
- The public status page shows a yellow "Scheduled maintenance" banner.
Windows are stored in maintenance_windows; the check worker looks up active
windows per monitor owner on every cycle.
Every Monday 9:00 UTC the cron worker calls POST /api/cron/digest. Each user
with a confirmed email alert channel receives a 7-day summary: per-monitor
uptime %, average response time, and incident count.
The app is installable as a PWA (/manifest.json, /icon.svg, /sw.js). In
Dashboard → Settings → Notifications users can enable browser push; critical
incident alerts arrive as persistent notifications that require manual dismiss.
- Requires
VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY/VAPID_SUBJECT(generate withnpx web-push generate-vapid-keys) - Subscriptions are stored in
push_subscriptions; dead endpoints are pruned automatically on send
supabase/migrations/003_v2_phase1.sql adds status_pages,
status_page_subscribers, maintenance_windows, and push_subscriptions with
RLS. It is idempotent and runs after the two V1 migrations.
Migrations 004_v2_phase2.sql (advanced checks + hardening), 005_v2_phase3.sql
(status-page power) and 006_v2_phase4.sql (teams) add the competitive surface:
- Failure thresholds — incidents open only after N consecutive failures (default 3) and resolve only after M consecutive recoveries (default 2), so a single flaky request never pages anyone. Per-monitor in the edit form.
- Advanced HTTP checks — expected status codes, keyword contains / not-contains, custom method / headers / body, per-monitor timeout, redirect following.
- SSL / TLS certificate monitoring — a dedicated check type that flags untrusted chains and certs expiring within a warning window (default 14 days). Certificate expiry is captured on every https check and shown on the monitor page.
- Per-monitor alert routing — route each Slack/email channel to specific monitors (or leave it global), set quiet hours (UTC) and choose whether a channel gets recovery alerts. Edit in Dashboard → Settings.
- Incident timeline — owners post updates to an open incident; they render on the public status page and are emailed to status-page subscribers.
- Status-page branding — brand color, logo URL and custom CSS per page (Settings → Status page), previously dead schema.
- Subscriber management + unsubscribe — view/remove subscribers in Settings; every subscriber email carries a one-click unsubscribe link.
- Status API + RSS + embed —
GET /api/status/[username]returns incidents, branding and maintenance;GET /api/status/[username]/rssis an incidents feed;/embed/[username]is an iframe-able live widget. - Custom domains — set a custom domain and verify a
_pingbeacon.<domain>TXT record;NEXT_PUBLIC_STATUS_DOMAINenables a sharedstatus.yourdomain.com/<username>subdomain (rewritten to/status/<username>). - Teams / workspaces — create teams, invite members by username, and share monitors. Members see shared monitors read-only; owners/editors can edit. There are no plan limits. Dashboard → Teams.
curl https://pingbeacon-five.vercel.app/api/status/youReturns monitors with current status, 90-day uptime %, avg response time, per-day uptime bars, any active incidents, and scheduled maintenance windows. No auth required.
Requires header x-cron-secret: <CRON_SECRET>, returns 401 otherwise.
Responds { ok, checked, up, down, alertsSent, errors }.
Requires the same x-cron-secret header. Sends the weekly digest email to
every user with a confirmed email alert channel. Responds { ok, sent, errors }.
Body { "email": "you@example.com" }. Creates an unconfirmed subscriber and
emails a confirmation link. Returns { ok: true }. Idempotent: re-subscribing
re-sends the confirmation email.
Query param token. Marks the subscriber confirmed. Returns { ok: true }.
Body is a full PushSubscription JSON (endpoint, keys.p256dh, keys.auth).
Saves a Web Push endpoint for the current user. Returns 503 when VAPID keys
are not configured.
Body { "endpoint": "..." }. Removes a Web Push endpoint.
Returns { ok, publicKey }, or 503 when Web Push is not configured.
RSS 2.0 feed of the status page's incident history.
Body { "message": "..." }. Posts an owner update to an open incident; it is
shown on the public timeline and emailed to status-page subscribers + routed
alert channels.
One-click unsubscribe for status-page subscribers (uses the confirm token).
Body { "domain": "status.example.com" }. Generates a TXT verification token
and checks DNS for it; returns the token + verified state.
Body { "username": "teammate" }. Adds a viewer membership to a team.
- Monitors never get checked — confirm the workflow ran (Actions tab) and
that
PINGBEACON_URL/CRON_SECRETsecrets match the server env. Test with the workflow's Run workflow button; the route returns401if secrets mismatch. - Email alerts don't arrive — Resend free tier can only email your own account until you verify a domain; check the function logs for the Resend status/error body.
- 401 from the cron route —
CRON_SECRETenv on Vercel differs from the GitHub secret, or the header name typo'd in a manual curl. - GitHub Actions free tier = 5 min checks — that is a GitHub scheduling
limit, not a PingBeacon one; set the workflow schedule to
* * * * *if your repository allows it. - Empty uptime bars / 0% — if a monitor was just created there is no data
yet; bars render grey (
no data) until the first checks arrive. - Push notifications don't arrive — confirm
VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY/VAPID_SUBJECTare set and that the site is served over HTTPS (Web Push requires a secure context). Then re-enable push in Settings → Notifications. - Subscriber emails don't arrive — the subscribe/confirm/incident emails use
Resend; without
RESEND_API_KEYthey silently skip and log. Verify your domain in Resend to reach arbitrary addresses. - Alerts fire during maintenance — the check worker suppresses alerting only
when a window in
maintenance_windowscovers the current time for that monitor's owner; check the window's timezone and that it is saved with a futureends_at.