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

> Source: <https://dev.to/merlonix/running-a-monitoring-saas-on-cloudflare-workers-supabase-for-almost-nothing-1hkm>
> Published: 2026-08-21 10:41:00+00:00

*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](https://merlonix.com/pricing/) — monitoring for agencies, from uptime and SSL through MCP server health. The [free tools](https://merlonix.com/tools/) run the same SSRF-guarded probe path described above; you can watch it work without signing up.
