From 62a79cd1373c59dac1e3161d3acc8a1551359964 Mon Sep 17 00:00:00 2001 From: devinderstockholm3219gmailcom Date: Mon, 5 Oct 2026 14:19:11 +0200 Subject: [PATCH] ci: query the database once a day so Supabase never pauses it Supabase paused the free project on 2026-10-05 and the API crash-looped, because the entrypoint's migrate deploy could not reach the database. The docs assumed the nightly cron jobs kept it awake, but they run inside the API process and only fire if Render is awake at 02:00. A scheduled workflow now calls /health/ready once a day: one SELECT 1 from GitHub's scheduler, independent of Render and the uptime pinger. No checkout, no third-party actions, empty permissions. A failed run emails the owner, so it doubles as an early warning. Docs: deployment.md caveat and ADR 53 amended with what actually happened. --- .github/workflows/keep-database-awake.yml | 50 +++++++++++++++++++++++ docs/architecture-decisions.md | 10 +++++ docs/deployment.md | 12 +++++- 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/keep-database-awake.yml diff --git a/.github/workflows/keep-database-awake.yml b/.github/workflows/keep-database-awake.yml new file mode 100644 index 0000000..dbda50f --- /dev/null +++ b/.github/workflows/keep-database-awake.yml @@ -0,0 +1,50 @@ +# keep-database-awake.yml — one real database query per day, so Supabase's +# free plan never pauses the project. +# +# Supabase pauses a free project after about a week without database activity +# (2026-10-05: it did, and the API crash-looped because `migrate deploy` in the +# entrypoint could not reach the database). The nightly cron jobs were meant +# to be that activity, but they run inside the API process — they only fire +# if Render happens to be awake at 02:00. This runs on GitHub's schedule +# instead, so it depends on neither Render nor the uptime pinger. +# +# Once a day is deliberate. Supabase's free database runs 24/7 whether it is +# queried or not, so one SELECT 1 a day costs nothing. What took Neon down +# (ADR 51) was a query every few seconds against an hourly compute quota. + +name: Keep database awake + +on: + schedule: + # 07:23 UTC. Off the hour on purpose: GitHub delays, and under load drops, + # scheduled runs queued at :00. One missed day is harmless — the pause + # needs seven in a row. + - cron: '23 7 * * *' + # Lets a run be started by hand to verify the setup. + workflow_dispatch: + +# The job only makes an outbound HTTPS request. No checkout, no third-party +# actions, no token — nothing to leak and nothing to hijack. +permissions: {} + +jobs: + ping: + name: Query the database through /health/ready + runs-on: ubuntu-latest + # Worst case: 4 attempts x 90 s + 3 x 20 s pauses = 7 minutes. + timeout-minutes: 10 + steps: + # /health/ready runs SELECT 1 and pings Redis (app.ts), and answers 503 + # when either fails. /health would NOT work here: it never touches the + # database, which is exactly why it is the one the pinger uses. + # + # --max-time 90: a sleeping Render instance takes about a minute to + # start, and Render holds the request until it is up. + # --retry-all-errors: retry a 503 or a timeout, not only network errors. + # --fail: a 503 must fail the job. GitHub emails a failed scheduled run + # to whoever last edited the cron line, so this is an early warning too. + - run: >- + curl --fail --silent --show-error + --max-time 90 + --retry 3 --retry-delay 20 --retry-all-errors + https://fakturly-api.onrender.com/health/ready diff --git a/docs/architecture-decisions.md b/docs/architecture-decisions.md index 713669c..46fe386 100644 --- a/docs/architecture-decisions.md +++ b/docs/architecture-decisions.md @@ -1084,6 +1084,16 @@ wrong CA. **Trade-off:** Supabase pauses a free project after about a week without database activity. The daily cron jobs are that activity; `/health` is not. +**Amended 2026-10-05 — that assumption failed.** The project was paused, and +the API crash-looped on `migrate deploy`. The cron jobs are `node-cron` inside +the API process, so they only run while Render's instance is awake at +02:00/03:00 — a dependency this ADR did not name. A scheduled GitHub Actions +workflow (`keep-database-awake.yml`) now calls `/health/ready` once a day. +That is one `SELECT 1`, from a scheduler that is independent of Render; on +Supabase's free plan, where the database runs 24/7 regardless, it costs +nothing. It does not contradict ADR 51 — that was about a query every few +seconds against an hourly compute quota. + --- ## 54. Supabase's Data API is locked out: RLS everywhere, no grants to anon diff --git a/docs/deployment.md b/docs/deployment.md index 3201db5..b1ce130 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -43,8 +43,14 @@ committed at `backend/certs/supabase-prod-ca-2021.crt`, SHA-256 > **Free-tier caveat:** Supabase pauses a free project after about a week > with no database activity. The daily cron jobs (overdue check, demo reset) -> query it every day, which counts. If it is ever paused: dashboard → -> project → **Restore**. Data is kept. +> were supposed to be that activity, but they run inside the API process and +> fire only if Render is awake at 02:00 — and on 2026-10-05 the project +> paused anyway. The API then crash-loops, because the entrypoint's +> `migrate deploy` cannot reach the database. The guarantee is now +> `.github/workflows/keep-database-awake.yml`: one `/health/ready` request a +> day from GitHub's scheduler, independent of Render and the uptime pinger. +> If it is ever paused regardless: dashboard → project → **Restore**. Data is +> kept, and Render recovers on its own once the database answers. > > **Neon still works** — set `DATABASE_URL` with its `?sslmode=require` and > *remove* `DATABASE_CA_CERT`; Neon's certificate is publicly trusted. @@ -101,6 +107,8 @@ committed at `backend/certs/supabase-prod-ca-2021.crt`, SHA-256 > keeps the compute awake 24/7. That is exactly how the live demo went down on > 2026-09-23: Neon paused the project for the rest of the month. `/health` > touches nothing but the process, so Render stays awake and Neon still sleeps. +> The keep-awake workflow is the one exception: it calls `/health/ready` once +> a *day*, because there a database query is the whole point. ## 3. Frontend — Vercel