docs/specs/2026-10-02-account-delete-design.md
111 lines · 5069 bytes
9 symbols in this file
1# Account deletion
2
3Closes #322. An account deletes itself. Today only `admin user delete`
4exists, and it refuses an account that anchors issues, merge requests,
5comments, reviews or an org with no other admin.
6
7## Decision
8
9`account delete` is a write on every surface. It takes the typed
10username, mails a confirmation link to the primary verified address,
11and on confirmation marks the account for deletion seven days out.
12During those seven days the account is refused everywhere except the
13two ways a person signs in, either of which cancels. After seven days
14a reaper purges it: owned content goes, authored content on other
15owners' repositories moves to a `ghost` account.
16
17## States
18
19| state | set by | SSH (full scope) | SSH (other scopes), API tokens | web session | web login link |
20|---|---|---|---|---|---|
21| requested | `account delete --confirm <name>` | normal | normal | normal | normal |
22| scheduled | the mailed link | cancels, then normal | refused | ended | cancels, then normal |
23| purged | the reaper | no account | no account | no account | no account |
24
25- *requested* is a row in `account_deletions` (user_id, token_hash,
26 requested_at, expires_at; the link lives 24 hours). Nothing about the
27 account changes. A second request replaces the first.
28- *scheduled* sets `users.delete_after` (now + 7 days) and ends every
29 web session. A full-scope SSH session or a completed login link clears
30 `delete_after`, is audited `account.delete.cancelled`, and prints or
31 shows "deletion of your account was cancelled". Runner-, read- and
32 deploy-scoped keys and API tokens are refused with "this account is
33 scheduled for deletion on <date>; sign in to cancel" so automation
34 cannot cancel by accident.
35- *purged*: the reaper, on the tick that already runs
36 `ReapPendingUsers`, takes every account with `delete_after` in the
37 past.
38
39## Purge
40
41One function in `internal/control` (it needs the repository root), run
42from the reaper, in this order:
43
441. Re-check the org rule. If the account is now the only admin of an
45 org, skip, leave `delete_after` set, audit
46 `account.delete.blocked`, and show it in `admin user show`. An
47 instance admin resolves it.
482. Delete every repository the account owns through `deleteRepo`, the
49 same path as `repo delete` (open MRs sourced from them are marked
50 source-gone; the directories go).
513. In one transaction: reassign `issues.author_id`,
52 `merge_requests.author_id`, `issue_comments.author_id`,
53 `mr_comments.author_id`, `mr_diff_comments.author_id` and
54 `mr_reviews.reviewer_id` to the ghost; then `DeleteUser`, whose
55 anchor check now passes. Everything else is already `CASCADE` (keys,
56 emails, tokens, sessions, snippets, memberships, watches, reactions,
57 assignments, inbox, review requests) or `SET NULL` (audit actor,
58 event actor, signatures, statuses, merged/closed by, releases).
59 Grants and the profile backfill row go in the same transaction, as
60 `DeleteUser` already does.
614. Audit `account.delete.purged` with the username.
62
63The username is free again after the purge. User ids are never reused
64(#306).
65
66## The ghost
67
68A real `users` row named `ghost`, created by the first purge that needs
69it and flagged `users.ghost = 1`: it has no keys, emails or sessions,
70cannot be signed in to, own anything, be granted access, or be deleted.
71`ghost` is added to `internal/policy/names.go` so nobody can register
72it. If an instance already has a real account named `ghost`, the purge
73refuses with a message telling the operator to rename it; gitbay.org
74has none. Its profile page reads "This account stands in for deleted
75users." Content shows `ghost` as its author, the way GitHub shows it.
76
77## Surfaces
78
79- `account delete --confirm <username>`: refuses a mismatched name
80 (exit 2), an account with no verified address (exit 4, "add and
81 verify an address first"), and the only admin of an org (exit 4,
82 naming the orgs). Prints where the mail went and suggests
83 `account export`.
84- `account delete --cancel`: clears a request or a schedule (the
85 scheduled case is reachable only from a full-scope key, which already
86 cancels on connect; this exists for the requested state).
87- Web: a "Delete account" section at the bottom of `/settings` with
88 the `confirmfield` partial, posting the same command. The mailed link
89 opens `/settings/delete?token=<token>`, a page that names the purge date
90 and has one button; the GET changes nothing.
91- The mail names the purge date, says how to cancel, and suggests
92 `account export`.
93- `admin user show` reports `delete_after` and a blocked purge.
94
95## Migration
96
97`account_deletions`; `users.delete_after TEXT`; `users.ghost INTEGER
98NOT NULL DEFAULT 0`.
99
100## Docs
101
102Parity row; Users wiki page (a "Deleting your account" section);
103`/privacy` text; Terms wiki page's "Leaving" section, which today says
104to mail the operator.
105
106## Not doing
107
108- Deleting authored content on other owners' repositories. Decided on
109 #322: it moves to the ghost.
110- An admin-initiated version with the grace period. `admin user delete`
111 stays as it is.