`apply` fails closed on an immutable field with "resolve manually" — which
meant a hand deletion in the Coolify UI, unscoped and unconfirmed, against an
instance whose token can see every project on it. That is how the wrong project
gets deleted.
`cast destroy <org>/<repo> --env <env> [--with-project]` is that act, scoped:
- MANIFEST-SCOPED. It deletes the resources the manifest declares in that
project and that environment, in reverse dependency order (applications →
services → databases). Anything else it finds is reported and LEFT STANDING —
that report is how a resource created outside cast gets discovered, and the
boxes in this fleet are multi-project by design.
- Not a flag on apply. `apply never deletes` is the invariant that makes it safe
to run on a schedule; apply.ts and diff.ts are untouched.
- REFUSES rather than no-ops: --all (always), a read-only instance, an absent
project (D-237 — an absent target must never read as a clean empty plan), a
manifest that declares nothing this environment holds, and --with-project
while anything undeclared is still in the project.
- The prod interlock lives in STATE, not argv: environments.<env>.destroy_allowed
in environments.yaml, absent = refuse. A flag is a thing you type without
reading; this is a line a human edits, commits and merges.
- The plan says what the delete COSTS: every database line carries its backup
schedule and when the last backup landed. A backups route cast cannot read
prints UNKNOWN and is treated as unrecoverable — it never rounds down to NONE.
- Last gate: the environment's name, typed (capture's ceremony).
Coolify's DELETE query params are sent explicitly (all four default to true):
delete_volumes, delete_connected_networks, delete_configurations — and
docker_cleanup=FALSE, because that one prunes the whole SERVER, and these boxes
host other people's production.