Cloudflare, Supabase and pg_cron: the stack behind a one-person back office
By Farough Darakhshan · 10/20/2026
For most of this year, the systems that run the back office of a water-damage restoration business lived on a Mac mini under my desk. Invoice tools, a dashboard poller, a job page, an assistant, a dozen scheduled scripts.
It worked until it didn't. When the Mac went to sleep, or its disk hung, everything stopped at once. The invoice engine was reachable only through a tunnel to that one machine. The poller that moved jobs from the claims dashboard to the engine stopped polling. Nobody got an error. Work just quietly didn't happen.
On 9/30/2026 I decided to move it. This is what the stack looks like now and the failures I found moving it, most of which were silent.
The rule: claim work runs on always-on cloud
I track 500 to 600 insurance claims at a time through six stages: track, invoice, send, defend against the adjuster's cuts, collect, and hand unpaid files to counsel.
The rule I set: anything that serves one of those six stages runs on always-on cloud infrastructure. That means Cloudflare, Supabase or Google Cloud. Nothing business-critical runs on a scheduled job on a Mac or behind a home tunnel.
The rest of the hardware still has a job:
- Shared web hosting runs WordPress sites, and only that.
- Local machines, including the three local-model boxes, take bulk work that can wait: overnight photo reading, document sorting. Each of those jobs has a cloud model as a fallback.
The claims dashboard I use most is built on Supabase. Cloudflare Workers handle the small services around it. Cloudflare D1, its SQLite database, holds the legacy claims records.
Failure one: schedules that never fired
On 10/01 I created a batch of new cloud jobs. Some were Cloudflare Workers with cron triggers. Some were GitHub repositories with an Actions schedule: block. Eight job repos in all.
By the end of that day, none of the scheduled runs had happened. The Worker crons on Workers created that day never fired. The Actions schedules in all eight repos missed their times. Both platforms had accepted the schedule. Neither reported an error. The dashboards showed jobs that looked scheduled and were dead.
So I picked one scheduler I could watch and moved everything to it.
The fix: pg_cron is the only scheduler
Supabase includes pg_cron, a scheduler that runs inside the Postgres database. Every cloud job is now triggered from there. A pg_cron entry does one of three things:
- POSTs to the Worker's authenticated
/runendpoint - calls GitHub's
workflow_dispatchAPI to start an Actions workflow - starts a Google Cloud Run job
Three rules came with it.
- One trigger per job. The Worker cron or the Actions schedule block gets removed. A second trigger is only allowed if the job has a run lock, so a double start is harmless.
- A job is not working until a scheduled run is observed. A manual run proves the code works. It proves nothing about the schedule. The first real proof was a pg_cron run at 09:00 UTC on 10/01.
- Anything else that schedules gets the same test. One Cloud Run job is driven by Google Cloud Scheduler, which showed as enabled. Enabled is not the same as fired, so its first run gets checked too.
The broader lesson is that a scheduler's "accepted" is not evidence. The only evidence is a run that happened at the time it was supposed to.
Failure two: D1 writes that wrote nothing
Cloudflare D1 is SQLite at the edge, and it is pleasant to work with. It also has sharp edges that look like silence.
On 9/7/2026, two separate debugging detours had the same cause. A write through wrangler d1 execute reported no rows changed and no error. The write had been rejected by a CHECK constraint, a rule on a column that lists the only values it accepts. Wrangler prints that failure in a shape a naive JSON parse throws away.
The traps were plausible English values that were not on the list. The column for a claim's work stage lists ten stages, from "Needs Review" to "Send to Legal". None of them means finished, so "Complete" is rejected. The column for the contracting company spells names with "dba" and no commas or periods, so the name as it prints on a contract is rejected.
How I diagnose it now: run the write one column at a time. The column that fails is the one with the constraint. Then read the table's SQL from sqlite_master and use the exact strings.
Failure three: a table rebuild that broke every foreign key
SQLite cannot change some column definitions in place. The usual pattern is to copy the table to a new one, drop the old one, and rename. On standard SQLite you guard it with PRAGMA legacy_alter_table = ON.
D1 accepts that pragma and ignores it. The rename step then rewrites the foreign key of every child table to point at the copy, and the migration drops the copy. Every child table now references a table that no longer exists.
I proved this on the remote database on 9/8/2026 with a throwaway parent and child table, before touching real data. The first real attempt failed and rolled back cleanly. An earlier migration had used the same pattern on the main claims tables and happened to come through. I do not read that as proof the pattern is safe.
Failure four: migrations split on semicolons
My D1 helper sends one statement per call, so a migration file gets cut into statements first. Cutting on every ; tears apart any string that contains a semicolon. On 9/17 a note with three semicolons in it broke an UPDATE while the INSERT after it applied normally. That reads as a half-applied migration.
Now note text uses commas, and files are cut on real statement boundaries.
What I would tell another small operator
- Put the work that earns money on infrastructure that does not sleep.
- Pick one scheduler you can see, and trust it only after a scheduled run.
- When a database write "succeeds" quietly, check the constraints before the code.
- Test a destructive migration pattern on a scratch table on the real platform first.
None of this is exotic. It is the plumbing that decides whether an invoice goes out on Tuesday or nobody notices it didn't.
I'm Farough (Frank) Darakhshan, and these are working notes from building a restoration back office. More projects are at faroughdarakhshan.com/projects, and the public code is on faroughdarakhshan.org.