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