Backup schedules are write-only on a premise that is false — GET /databases/{uuid}/backups exists, so a rebuilt database silently has no backups #51

Closed
opened 2026-07-14 20:54:58 +00:00 by dan-claude-bot · 0 comments
dan-claude-bot commented 2026-07-14 20:54:58 +00:00 (Migrated from github.com)

The claim, and why it is worth re-testing

docs/semantics.md:606 files this under "Known limitations, not defects":

Backup schedules are create-time only. A manifest database's backup block (frequency, retention) is applied only when the database is first created; it is deliberately kept out of the diffed fields (live Coolify state doesn't expose it back, so diffing it would flag spurious drift every run — breaking idempotency). Changing a schedule on an existing database is a runbook act, done by hand in the Coolify UI.

The parenthesis is the load-bearing part, and it is not true as stated. It is true that a backup schedule is not on the database's own GET — but the vendored reference/coolify-openapi-4.1.2.json carries a dedicated route for exactly this:

GET    /databases/{uuid}/backups                              ← list a database's schedules
POST   /databases/{uuid}/backups                              ← what apply already calls (cli.ts:2080)
PATCH  /databases/{uuid}/backups/{scheduled_backup_uuid}
DELETE /databases/{uuid}/backups/{scheduled_backup_uuid}
GET    /databases/{uuid}/backups/{scheduled_backup_uuid}/executions

cast already POSTs to that path to create the schedule. It has simply never GET'd it. "Coolify doesn't expose it back" appears to mean "it isn't on the resource I happened to read."

(The spec documents the GET's response body as "Content is very complex. Will be implemented later." — so the shape needs a live probe before implementing, the same way #46 needed one. That is a reason to probe, not a reason to conclude it cannot be read.)

What the limitation costs today

Backups are the only thing standing between a database and unrecoverable loss, and they are the one field of a database that cast writes and then never looks at again. Concretely:

  • A database created before its backup: block was declared never gets one. apply sets the schedule only inside the create branch (cli.ts:2073-2086). Add backup: to a manifest for an existing Postgres, run apply, get a clean run and zero backups. Nothing says so.
  • A schedule deleted or changed in the UI is invisible. diff cannot see it, so the drift is not drift — it is nothing.
  • The --full diff that gates a production cutover passes with an unbacked-up production database. This is not hypothetical; the prod box's inventory row currently carries "⚠️ unverifiable by any tool, still owed: the postgres backup schedule (dropped from diff)" as a standing manual debt (heavy-duty/incubator#10).
  • semantics.md:541 states the consequence plainly and then accepts it: "backup schedules (not exposed on a database's GET — a rebuild has no backups until you declare them)" — in the section about disaster recovery.

The DR argument is the one that settles it: the state of the world after a "rebuild from state" is a database with no backup schedule, and the tool reports that state as clean.

Suggested

  1. Probe the GET against a live Coolify holding a database with a schedule, and record the body shape in the issue (GET /api/v1/databases/{uuid}/backups).
  2. Read it into the live side and diff frequency + retention like any other field — the spurious-drift fear in semantics.md is exactly what a real read makes go away.
  3. Make apply set it on update, not only on createPATCH /databases/{uuid}/backups/{uuid} exists. Declaring backup: on an existing database should start backing it up, which is what every reader of that manifest already assumes it does.
  4. If the probe shows the GET genuinely cannot be read — then the honest fallback is the one cast already uses for a service's domains (resolve.ts:389): say so on every run, out loud, rather than dropping the field silently. A declared backup: that cast cannot verify should print backup schedule declared, NOT compared — verify in the Coolify UI, on the same principle as the placement line added in #41: the assumption goes on screen at the moment it is being made.
  5. Either way, correct semantics.md — the "not exposed" claim is what stopped anyone looking.
  • Same family as #41's second half (a write-only field whose silence is the defect) and #46 (a "the API can't do this" belief that was never probed).
## The claim, and why it is worth re-testing `docs/semantics.md:606` files this under **"Known limitations, not defects"**: > **Backup schedules are create-time only.** A manifest database's `backup` block (`frequency`, `retention`) is applied only when the database is first created; it is deliberately kept out of the diffed `fields` (**live Coolify state doesn't expose it back**, so diffing it would flag spurious drift every run — breaking idempotency). Changing a schedule on an existing database is a runbook act, done by hand in the Coolify UI. The parenthesis is the load-bearing part, and it is **not true as stated**. It is true that a backup schedule is not on the database's own `GET` — but the vendored `reference/coolify-openapi-4.1.2.json` carries a **dedicated route for exactly this**: ``` GET /databases/{uuid}/backups ← list a database's schedules POST /databases/{uuid}/backups ← what apply already calls (cli.ts:2080) PATCH /databases/{uuid}/backups/{scheduled_backup_uuid} DELETE /databases/{uuid}/backups/{scheduled_backup_uuid} GET /databases/{uuid}/backups/{scheduled_backup_uuid}/executions ``` cast **already POSTs to that path** to create the schedule. It has simply never GET'd it. "Coolify doesn't expose it back" appears to mean "it isn't on the resource I happened to read." (The spec documents the GET's response body as *"Content is very complex. Will be implemented later."* — so the shape needs a live probe before implementing, the same way #46 needed one. That is a reason to probe, not a reason to conclude it cannot be read.) ## What the limitation costs today Backups are the **only** thing standing between a database and unrecoverable loss, and they are the one field of a database that cast writes and then never looks at again. Concretely: - **A database created before its `backup:` block was declared never gets one.** `apply` sets the schedule *only* inside the create branch (`cli.ts:2073-2086`). Add `backup:` to a manifest for an existing Postgres, run `apply`, get a clean run and zero backups. Nothing says so. - **A schedule deleted or changed in the UI is invisible.** `diff` cannot see it, so the drift is not drift — it is nothing. - **The `--full` diff that gates a production cutover passes with an unbacked-up production database.** This is not hypothetical; the prod box's inventory row currently carries *"⚠️ unverifiable by any tool, still owed: the postgres backup schedule (dropped from `diff`)"* as a standing manual debt ([heavy-duty/incubator#10](https://github.com/heavy-duty/incubator/pull/10)). - **`semantics.md:541` states the consequence plainly and then accepts it:** *"backup schedules (not exposed on a database's GET — **a rebuild has no backups until you declare them**)"* — in the section about disaster recovery. The DR argument is the one that settles it: the state of the world after a *"rebuild from state"* is a database with no backup schedule, and the tool reports that state as **clean**. ## Suggested 1. **Probe the GET** against a live Coolify holding a database with a schedule, and record the body shape in the issue (`GET /api/v1/databases/{uuid}/backups`). 2. **Read it into the live side** and diff `frequency` + `retention` like any other field — the spurious-drift fear in `semantics.md` is exactly what a real read makes go away. 3. **Make `apply` set it on update, not only on create** — `PATCH /databases/{uuid}/backups/{uuid}` exists. Declaring `backup:` on an existing database should start backing it up, which is what every reader of that manifest already assumes it does. 4. **If the probe shows the GET genuinely cannot be read** — then the honest fallback is the one cast already uses for a service's domains (`resolve.ts:389`): **say so on every run**, out loud, rather than dropping the field silently. A declared `backup:` that cast cannot verify should print `backup schedule declared, NOT compared — verify in the Coolify UI`, on the same principle as the placement line added in #41: *the assumption goes on screen at the moment it is being made.* 5. Either way, **correct `semantics.md`** — the "not exposed" claim is what stopped anyone looking. ## Related - Same family as #41's second half (a write-only field whose silence is the defect) and #46 (a "the API can't do this" belief that was never probed).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/cast#51
No description provided.