October 31, 2026
I open-sourced Upzy: uptime monitoring and attack-surface security in one tool
Why I built it, how it works inside (Postgres job queue, SSRF-safe probes, an A–F grade), and what I learned taking a private repo public

By Aditya Rajbhar
5 min read
Today I made Upzy public under the MIT license. It's uptime monitoring and external attack-surface security for small teams, and it tries hard to tell you, in plain English, what's wrong and exactly how to fix it.
This post covers why it exists, how it works inside, and what I learned getting a private codebase ready to be public.
Why one tool for both
Small teams usually end up with two tools. A cheap uptime pinger that knows nothing about what you expose, and (if they can afford it) a security scanner written for security teams, which reports problems in jargon and never tells you whether the site is even up.
I wanted one place that answers two questions: is it up? and what am I exposing? And for every problem, the change to make for my actual stack, not a link to an RFC.
Here's what you see about a minute after adding a website:
What it does
Uptime. HTTP, TCP, DNS and SSL checks every minute, keyword and JSON assertions, multi-step API flows, and heartbeat monitors for cron jobs and workers. A slow-but-up service gets a "degraded" state instead of an incident.
Attack surface. Certificates and old TLS versions, security headers and cookies, SPF, DMARC, CAA and MTA-STS, DNSSEC, domain expiry, dangling DNS (subdomain takeover), new third-party scripts appearing on a page, and known CVEs for the software a site runs, enriched with CISA KEV and EPSS so they're ranked by real-world risk.
A fix, not a report. Every finding comes with the change to make for the stack Upzy detected (nginx, Apache, Caddy, Cloudflare, Vercel and more), and a Verify fix button re-runs just that check.
A grade. Every site gets an A–F health score with a breakdown and a 90-day trend, so "are we OK?" has a one-letter answer.
Status pages and alerts. Public status pages with 90 days of uptime and email subscribers. Alerts go to email, Slack, Discord, Microsoft Teams, PagerDuty, Opsgenie, GitHub issues or signed webhooks.
AI, opt-in. If you add your own provider key (Groq, OpenAI, Anthropic, Gemini or OpenRouter), it explains findings in plain English, suggests a likely cause when a monitor goes down and drafts postmortems. There's also a read-only MCP server, so tools like Claude Code can ask "which monitors are down?" or "which findings should I fix first?".
How it works inside
The stack is Bun workspaces with Turborepo, an Elysia API, Drizzle on Postgres, better-auth, and Next.js 16 with shadcn/ui. A few design decisions I'm happy with:
Postgres is also the job queue
There's no Redis or separate broker. Jobs are rows in a jobs table, and workers claim them with FOR UPDATE SKIP LOCKED, so two workers never get the same job:
update jobs set state = 'running', attempts = attempts + 1, locked_at = now()
where id in (
select id from jobs
where queue = $1 and state = 'queued' and run_at <= now()
order by run_at
limit $2
for update skip locked
)update jobs set state = 'running', attempts = attempts + 1, locked_at = now()
where id in (
select id from jobs
where queue = $1 and state = 'queued' and run_at <= now()
order by run_at
limit $2
for update skip locked
)Instead of polling, an insert trigger sends a Postgres notification and the waiting worker wakes up through LISTEN (with a fallback to polling every second where LISTEN isn't available). One less service to run is a big deal when the goal is "self-host it on one small VPS".
One worker, several roles
The worker runs roles: a scheduler that decides what's due, a prober that runs uptime checks, a scanner for security checks, a notifier that delivers alerts, and a few more. An environment variable (WORKER_ROLES) picks which ones a process runs, so everything can live on one machine, or a role can move to another host that can reach Postgres.
Blips don't page you
A monitor is only declared down after a number of consecutive failures (two by default), and any success resolves it. Being slow never opens an incident; it only shows "degraded". It's a small pure state machine, which made it easy to test every edge case, including maintenance windows.
Targets are public only
A monitoring tool that fetches any URL you give it is an SSRF tool if you're careless. Upzy refuses loopback, private, link-local and cloud-metadata addresses, and it resolves and pins the address at connect time, so DNS rebinding can't swap it afterwards.
Passive until you prove ownership
Without proof that you own a domain, scans only look at what any visitor can see: no logins, no guessing, no port scans. After you verify it with a DNS record or a file, scans also request a short fixed list of paths that should never be public (like /.env and /.git/HEAD) and test whether about a dozen risky ports (databases, RDP, Telnet) accept a connection. Findings record where something was found, never what was read.
AI is fenced in
The AI layer never has write access, and alerting never depends on it. Inputs are redacted and wrapped as untrusted data, output is schema-validated, and each organization has a daily token budget. It runs only on your own key, with no shared server key and no silent fallback to another provider.
What I learned open-sourcing it
The code was the easy part. Making a private repo safe to publish took more care than I expected. What I did:
- Split the private stuff out first. Plans, internal docs and notes moved to a separate private repo, so the public one only has what belongs there.
- Audited every config and script for anything that shouldn't be public, not just
.envfiles. - Published from a clean snapshot with one squashed commit. A private repo's history can contain things you deleted long ago. Starting the public repo from a single fresh commit means old history can't leak anything.
CI on the public repo runs format, lint, types, migrations (including a drift check), the full test suite against Postgres and the build, plus a separate workflow with gitleaks, OSV-Scanner and CodeQL.
Try it
- Code (MIT): github.com/codebyaadi/upzy-oss
- Hosted version: upzy.codebyaadi.com (early access, free)
It runs locally with Bun and Docker in a few commands, and self-hosts on one small VPS with Docker Compose and Caddy. The README has the steps.
What's next: DKIM key checks. A comment on my launch post asked about email authentication, and the first version is already written.
If you try it, I'd love to hear what it finds on your domain, and what it gets wrong.