Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/keep-database-awake.yml
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions docs/architecture-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
Loading