feat: delete a team seat for good, once it is disabled

A disabled account persists indefinitely. Removing it is an explicit request,
never a timer, and it is refused on a live account with
`account_must_be_disabled_first` — which puts a reversible step in front of the
only irreversible operation in the feature.

The audit row is written before `cascadeDelete` runs. The FKs are ON DELETE SET
NULL and the `_keep` columns carry the identifiers, so the record of what was
done survives the account it names.

No second billing emit here: `cascadeDelete` already captures the seat and
fires `team.account.deleted` through UserAccountService, and emitting again
would close the storage charge twice. Disabling closed the per-account charge;
this closes the storage one, and it is the only thing that does.

There is no restore window, and none was wanted: the reversible step already
exists earlier at disable, a disabled account costs only the bytes it holds so
nothing pressures a hasty delete, and a restore promise means retaining data
the team explicitly asked to be rid of.

Published in rate-limits-and-quotas.md alongside the team-deletion note,
since the two are easy to confuse and only one of them frees a seat.

Closes PUT-1732.
This commit is contained in:
Juan Castro
2026-09-09 11:51:44 -04:00
parent 81d15f412b
commit a60a4a98b4
5 changed files with 112 additions and 0 deletions
+2
View File
@@ -200,6 +200,8 @@ A reset returns a temporary password once and never again. It stops working 24 h
Deleting a team frees the owner's slot, but it does **not** free the seats: the accounts it created still exist, still hold their files, and keep their usernames. They are disabled, not removed — deleting a team is not a way to stop paying for the accounts in it.
Removing a seat for good is a separate, explicit request, and it is refused unless the account is already disabled (`account_must_be_disabled_first`). That ordering puts a reversible step in front of the only irreversible operation in the feature. **There is no restore window**: deletion removes the files, returns the username to the pool, and invalidates every credential. Nothing expires a disabled account on a timer — it persists, costing only the bytes it holds, until someone asks for it to go.
Lowering the seat limit never disables anyone. A team already above a reduced limit keeps every account it has and is simply refused new ones until it is back under.
Both limits are per deployment (`max_teams_per_user`, `max_seats_per_team`) rather than per team, so raising them moves every team at once.