{"slug": "running-a-monitoring-saas-on-cloudflare-workers-supabase-for-almost-nothing", "title": "Running a Monitoring SaaS on Cloudflare Workers + Supabase for Almost Nothing", "summary": "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.", "body_md": "*Originally published on the Merlonix blog.*\n\nMerlonix 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.\n\nThis 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.\n\n**Compute: eight Cloudflare Workers.** One HTTP API worker (Hono) serves everything under `api.merlonix.com`\n\n. 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.\n\n**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).\n\n**Glue: Cloudflare Queues.** The scheduler enqueues due work onto `checks-q`\n\nand `vendor-q`\n\n; 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.\n\n**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.\n\nThe scheduler's production cron fires **every minute**. But firing and working are different things:\n\n```\n/** Revenue-gated SLA throttle. */\nexport function isSweepDue(now: Date, hasLiveSubscription: boolean): boolean {\n  if (hasLiveSubscription) return true;\n  return now.getUTCMinutes() % 5 === 0;\n}\n```\n\nWith 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.\n\nThe interesting part is the failure mode we shipped and later fixed. The `hasLiveSubscription`\n\ncheck queries the subscriptions table, and the original catch block was bare:\n\n```\n} catch { hasLiveSubscription = false; }\n```\n\nRead 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.**\n\nOur 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`\n\n, the cloud metadata endpoint, where a successful internal request leaks the execution environment's credentials.\n\nWorkers add a twist: `fetch()`\n\ndoesn't expose the resolved IP and gives you no way to pin one. So the guard works like this:\n\n`http://127.0.0.1`\n\n; we re-run the full SSRF check on the `Location`\n\nheader 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()`\n\nperforms 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.\n\nA 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.\n\nSo the external watchdog doesn't run on Cloudflare at all. It runs *inside Supabase* — which sits on AWS — using `pg_cron`\n\nplus the `http`\n\nextension: every 5 minutes, a `SECURITY DEFINER`\n\nfunction curls `merlonix.com`\n\nand the API's `/healthz`\n\nand `/readyz`\n\nendpoints, 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**.\n\n`worker_runs`\n\nledger (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.\n\nThe pattern that generalizes: **cron fires cheap and constant; a pure function decides whether the tick does work.** It gives you a testable throttle (`isSweepDue`\n\nis 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.\n\nThe 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.", "url": "https://wpnews.pro/news/running-a-monitoring-saas-on-cloudflare-workers-supabase-for-almost-nothing", "canonical_source": "https://dev.to/merlonix/running-a-monitoring-saas-on-cloudflare-workers-supabase-for-almost-nothing-1hkm", "published_at": "2026-08-21 10:41:00+00:00", "updated_at": "2026-08-21 11:16:45.148427+00:00", "lang": "en", "topics": ["developer-tools", "ai-infrastructure"], "entities": ["Merlonix", "Cloudflare Workers", "Cloudflare Pages", "Cloudflare Queues", "Supabase", "Hono", "Next.js", "Sentry"], "alternates": {"html": "https://wpnews.pro/news/running-a-monitoring-saas-on-cloudflare-workers-supabase-for-almost-nothing", "markdown": "https://wpnews.pro/news/running-a-monitoring-saas-on-cloudflare-workers-supabase-for-almost-nothing.md", "text": "https://wpnews.pro/news/running-a-monitoring-saas-on-cloudflare-workers-supabase-for-almost-nothing.txt", "jsonld": "https://wpnews.pro/news/running-a-monitoring-saas-on-cloudflare-workers-supabase-for-almost-nothing.jsonld"}}