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