cd /news/developer-tools/running-a-monitoring-saas-on-cloudfl… · home topics developer-tools article
[ARTICLE · art-105858] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Running a Monitoring SaaS on Cloudflare Workers + Supabase for Almost Nothing

Merlonix, a monitoring SaaS, runs its entire infrastructure on Cloudflare Workers and Supabase for near-zero cost. The company uses eight Cloudflare Workers for compute, static Next.js exports for the frontend, Cloudflare Queues for job processing, and a single Supabase Postgres database with forced row-level security. A revenue-gated SLA throttle automatically adjusts monitoring frequency based on subscription status, and the team fixed a silent degradation failure mode by making graceful degradation loud.

read5 min views5 publishedAug 21, 2026

Originally published on the Merlonix blog.

Merlonix monitors uptime, SSL/TLS, DNS, email authentication, blacklists, Certificate Transparency, Core Web Vitals, and MCP servers for agencies. A monitoring product has an unforgiving shape: it must run continuously, hit arbitrary customer-supplied hostnames, and stay up more reliably than the things it watches — while, in our case, keeping the infrastructure bill within a rounding error of zero until revenue exists to justify more.

This post is the real architecture, including the parts that bit us. Nothing here is a reference design we aspire to; every component named below is deployed and verifiable from the outside.

Compute: eight Cloudflare Workers. One HTTP API worker (Hono) serves everything under api.merlonix.com

. Seven background workers do the actual monitoring: a scheduler (cron), a check-runner and vendor-runner (queue consumers that execute checks), a vendor-fetcher, a dlq-consumer (dead-letter forensics), a browser-runner, and a db-backup worker. Each has its own wrangler config and its own deploy verifier.

Frontend: two static Cloudflare Pages projects. The marketing site and the app are both Next.js static exports — no SSR servers, no origin to fall over. Anything dynamic goes through the API worker. A Pages Function provides the thin middleware layer (redirects, custom-domain status-page routing).

Glue: Cloudflare Queues. The scheduler enqueues due work onto checks-q

and vendor-q

; the runners consume in batches of 10. Failures retry, and exhausted retries land in a dead-letter queue with a consumer that records the forensic payload instead of dropping it.

State: one Supabase Postgres. Every table that holds tenant data runs with forced row-level security — the API worker uses the service role deliberately and narrowly, and RLS is audited by a script that enumerates deny-all tables and cross-tenant probes. The migration ledger is past 270 forward-only migrations, applied to production by an idempotent runner. There is no second database; the queue messages carry IDs, and Postgres is the single source of truth.

The scheduler's production cron fires every minute. But firing and working are different things:

/** Revenue-gated SLA throttle. */
export function isSweepDue(now: Date, hasLiveSubscription: boolean): boolean {
  if (hasLiveSubscription) return true;
  return now.getUTCMinutes() % 5 === 0;
}

With no live customer, a sweep only does real work on every 5th UTC minute — byte-for-byte the enqueue volume of a 5-minute cron, which costs effectively nothing while there's nobody to monitor but seeded assets. The moment any revenue-bearing subscription exists, the very next tick restores full per-minute cadence. No redeploy, no flag flip, no human. The SLA follows the money automatically.

The interesting part is the failure mode we shipped and later fixed. The hasLiveSubscription

check queries the subscriptions table, and the original catch block was bare:

} catch { hasLiveSubscription = false; }

Read that as an SRE: a transient database blip on this one query would silently throttle paying customers' monitoring cadence from 1 minute back to 5 — no error, no alert, checks just quietly late. It's the worst kind of degradation: invisible, revenue-adjacent, and plausible-deniable. The fix keeps the safe degradation (bootstrap mode beats crashing the sweep) but fans the failure out to Sentry and structured logs under its own error code, so a sustained degradation pages before a customer notices. If you take one pattern from this post: when you degrade gracefully, make the degradation loud.

Our free tools and monitoring checks fetch URLs and hostnames that strangers type into a form. That is the textbook server-side request forgery setup: the classic target is 169.254.169.254

, the cloud metadata endpoint, where a successful internal request leaks the execution environment's credentials.

Workers add a twist: fetch()

doesn't expose the resolved IP and gives you no way to pin one. So the guard works like this:

http://127.0.0.1

; we re-run the full SSRF check on the Location

header before following, and a second hop is always rejected.And the honest residual, documented in the code rather than papered over: the runtime's fetch()

performs its own DNS resolution, so a sub-TTL attacker could answer our DoH probe with a public IP and the runtime with a private one. IP-pinning isn't viable on Workers (TLS validates against SNI). The authoritative backstop is Cloudflare's platform egress policy — Workers cannot open connections into loopback/RFC1918/link-local regardless of DNS. Our DoH layer is defense-in-depth on top of that, and we say so, because an SSRF guard you overstate is worse than one you understand.

A monitoring company that monitors itself with itself has a bootstrapping problem: if Cloudflare has an account-level bad day, the thing that would tell us is also having a bad day.

So the external watchdog doesn't run on Cloudflare at all. It runs inside Supabase — which sits on AWS — using pg_cron

plus the http

extension: every 5 minutes, a SECURITY DEFINER

function curls merlonix.com

and the API's /healthz

and /readyz

endpoints, tracks consecutive failures in a table, and posts to an operator Discord webhook after a sustained-failure threshold, with a single recovery notice when things come back. No third-party account, no additional bill, and — the actual point — no shared fate with the platform it watches.

worker_runs

ledger (worker name, outcome, duration). "Is the scheduler actually running?" is a SQL query, not a guess — and a failed heartbeat Nearly nothing, and that's a design constraint, not an accident. Static Pages sites are free. Workers requests at our current scale sit comfortably inside the Workers plan floor. Supabase is on the free tier — the watchdog cron and RLS-forced Postgres both fit inside it. The paid-API checks that could cost real money (PageSpeed Insights, LLM-backed features) sit behind explicit flags, daily caps, and per-tenant meters, so the worst case of a bug is a rate-limit, not a bill. The whole stack is engineered so the monthly infrastructure bill stays within a rounding error of zero until customers exist — at which point the same cron throttle that saves money today upgrades their SLA on the next tick.

The pattern that generalizes: cron fires cheap and constant; a pure function decides whether the tick does work. It gives you a testable throttle (isSweepDue

is three lines and unit-tested), a zero-redeploy upgrade path, and one place where cadence policy lives. Pair it with loud degradation, verify every deploy from the outside, and put your last-resort watchdog on somebody else's cloud.

The product this architecture serves is Merlonix — monitoring for agencies, from uptime and SSL through MCP server health. The free tools run the same SSRF-guarded probe path described above; you can watch it work without signing up.

── more in #developer-tools 4 stories · sorted by recency
── more on @merlonix 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/running-a-monitoring…] indexed:0 read:5min 2026-08-21 ·