Add ability to soft-delete an Organization
## Context
As part of the TLG-to-Organization onboarding flow, we want to begin auto-transferring Top-Level Groups (TLGs) to Organizations. Once in place, customers will be able to combine multiple TLGs into a single Organization via an onboarding experience.
We already have tooling to transfer groups from one Organization to another. However, there is currently **no way to soft-delete an Organization** — which is required when merging TLGs: after transferring all groups out of a redundant Organization, that Organization needs to be soft-deleted.
## Proposal
Add the ability to soft-delete an Organization, reusing the same soft-delete pattern already used for namespace (i.e. groups & projects).
### Behavior
- **Visibility:** a soft-deleted Organization should appear as non-existent to all users except admins.
- **Reverting:** soft-deletion can only be reverted by admins or via a Rails console. No dedicated restoration UI is required.
- **Eligibility:** soft-deletion is only allowed when the Organization is fully empty — no groups, no projects, and no org-level entities (e.g. artifacts from the upcoming org-level Artifact Registry).
- **Roles:** Organization owners and instance admins can trigger soft-deletion.
## DRI
@rymai
## Implementation plan
Organizations are not namespace-hierarchical (no parent/ancestors), so the state machine is simpler than `Namespaces::Stateful`. There is no `ancestor_inherited` state, no archival, and no state preservation needed (cancel always returns to `active`).
### States
| State | Integer value | Meaning |
|-------|:---:|---------|
| `unconfirmed` | 0 | Newly created; not yet usable. |
| `soft_deleted` | 1 | Soft-deleted. |
| `deletion_in_progress` | 2 | Deletion worker has started processing. |
| `confirmed` | 3 | Organization owner has confirmed the structure; background provisioning is running. |
| `active` | 4 | Background provisioning complete; fully operational. |
### Transition guards and callbacks summary
| Transition | Guards | Before callbacks | After callbacks |
|-----------|--------|-----------------|-----------------|
| `confirm` (unconfirmed → confirmed) | `confirmed_by_user` present | `update_state_metadata`, `ensure_confirmed_by_user`, `set_confirmation_data` | `log_transition` |
| `activate` (confirmed → active) | — | `update_state_metadata` | `log_transition` |
| `soft_delete` (active → soft_deleted) | `transition_user` present; organization must be empty | `update_state_metadata`, `ensure_transition_user`, `ensure_organization_is_empty`, `set_soft_deletion_data` | `log_transition` |
| `restore` (soft_deleted → active) | — | `update_state_metadata`, `clear_soft_deletion_data` | `log_transition` |
| `hard_delete` (soft_deleted → deletion_in_progress) | — | `update_state_metadata` | `log_transition` |
| `abort_hard_deletion` (deletion_in_progress → soft_deleted) | - | `update_state_metadata`, `ensure_transition_user` | `log_transition` |
| Any (failure) | — | — | `update_state_metadata_on_failure`, `log_transition_failure` |
### State transition diagram
```mermaid
stateDiagram-v2
direction LR
unc: unconfirmed
con: confirmed
act: active
sd: soft_deleted
dip: deletion_in_progress
[*] --> unc : (organization created)
unc --> con : confirm
con --> act : activate
act --> sd : soft_delete
sd --> act : restore
sd --> dip : hard_delete
dip --> sd : abort_hard_deletion
dip --> [*]
```
Here's a Claude' summarized paragraph based on the notes from https://gitlab.com/groups/gitlab-org/-/work_items/19733+ (consolidated into this epic):
- Peter Hegman raised the question of whether deletion is needed for Protocells, noting its relation to users potentially moving back from a protocell to the legacy cell.
- Jennifer Black clarified that two kinds of rollback are being considered: a technical rollback from Cells to the legacy DB, and a customer-facing opt-out of Organizations entirely.
- Matt Andrews raised a legal/data concern — whether customers have a contractual right to have their data fully removed, and whether launching without this capability would breach any commitments.
- Peter Hegman noted that existing group- and project-level deletion already covers most data, and that the only truly org-specific data today is the organization name, description, and avatar.
- Drew Blessing emphasized the broader complexity: deleting an organization is essentially deleting an entire instance — all contained groups, projects, and users — and suggested a short-term approach of an instance-admin-only endpoint that only works once all users have been removed, while acknowledging that a longer-term, more carefully designed solution is needed.
---
<!-- STATUS NOTE START -->
## Status 2026-06-11
Good progress this week — three child issues closed and the hard-delete service path landed. Still on track for %"19.1".
:clock1: **total hours spent this week by all contributors**: ~18
:tada: **achievements**:
- State filtering landed in https://gitlab.com/gitlab-org/gitlab/-/work_items/594312+s via https://gitlab.com/gitlab-org/gitlab/-/merge_requests/235559+s — organizations being deleted are now hidden from non-admins, while members keep visibility through the normal lifecycle.
- The `OrganizationDelete` GraphQL mutation https://gitlab.com/gitlab-org/gitlab/-/work_items/594313+s was completed (community-contributed) and closed.
- The hard-delete service and worker https://gitlab.com/gitlab-org/gitlab/-/work_items/594310+s merged via https://gitlab.com/gitlab-org/gitlab/-/merge_requests/238553+s, completing the soft-delete → hard-delete lifecycle on the backend.
- Resolved the AI-usage-events foreign-key question https://gitlab.com/gitlab-org/gitlab/-/work_items/601838+s: rather than a per-table fix, we opted to keep the existing constraint for now (safe while Organizations is feature-flagged) and proposed a generic "delete child records first" framework in handbook MR https://gitlab.com/gitlab-com/content-sites/handbook/-/merge_requests/20016+.
:issue-blocked: **blockers**:
- None.
:arrow_forward: **next**:
- Merge the REST API soft-delete endpoint https://gitlab.com/gitlab-org/gitlab/-/work_items/599345+s (in review).
- Clean up `organization_users` before destroying an organization at hard-delete time https://gitlab.com/gitlab-org/gitlab/-/work_items/601755+s.
- The rest of the work items in the epic are nice to have for %"19.1" but aren't blockers for the reconciliation flow.
_Copied from https://gitlab.com/groups/gitlab-org/-/epics/21433#note_3444047346_
<!-- STATUS NOTE END -->
epic