| 1 | # 0020 — Deleting a repository is a tombstone, then a purge |
| 2 | |
| 3 | **Status:** adopted |
| 4 | |
| 5 | ## Decision |
| 6 | |
| 7 | `DELETE /api/repositories/:username/:slug` removes everything the repository |
| 8 | stored before it answers 204. In order: |
| 9 | |
| 10 | 1. **Tombstone.** The index is replaced, by compare-and-swap, with an empty one |
| 11 | whose `deleted` flag is set. From here every read and every push sees the |
| 12 | repository as absent. |
| 13 | 2. **Purge, in parallel.** Every log entry under |
| 14 | local cache directory (after any replay in flight has settled), and the code |
| 15 | search shards and marker (after any index run in flight has finished). |
| 16 | 3. **Drop the row.** Issues, pull requests, stars and every index table cascade. |
| 17 | |
| 18 | If any step fails the request fails, the row stays, and the repository is |
| 19 | tombstoned: unreadable, still listed, and deletable again. Every step is |
| 20 | idempotent, so the retry finishes the job. |
| 21 | |
| 22 | A repository that heads an open pull request into another repository cannot |
| 23 | be deleted (409). Closed and merged requests it opened are kept: their |
| 24 | `headRepositoryId` is set to null, so the base repository keeps the title, |
| 25 | body, comments and timeline. A merged request still shows its commits and |
| 26 | diff, because the merge copied them into the base (see |
| 27 | [0016](0016-a-pull-request-spans-two-logs.md) |
| 28 | none, since its commits existed only in the deleted repository. A request |
| 29 | opened in the moment between that check and the tombstone is closed in the |
| 30 | same transaction that drops the row, so an open request always has a head. |
| 31 | |
| 32 | ## Why |
| 33 | |
| 34 | Nothing of a repository may outlive the owner's decision to delete it. Postgres |
| 35 | and object storage share no transaction, so "atomic" cannot mean one commit |
| 36 | across both. It means: a 204 is only sent once nothing is left, and anything |
| 37 | short of that is visible to the owner and resumable by them. |
| 38 | |
| 39 | The tombstone is what makes the purge safe. A push that raced the delete has |
| 40 | either committed before the tombstone, so its entry is listed and purged, or |
| 41 | loses its compare-and-swap to it, reads the repository as gone, and deletes its |
| 42 | own entry before failing (`PushTransactionService`). Deleting the index instead |
| 43 | would not fence anything: a push authorized a moment earlier would find no |
| 44 | index and create a fresh one under the dead id. |
| 45 | |
| 46 | Blocking on open requests protects work in progress in someone else's |
| 47 | repository; keeping closed and merged ones keeps the base repository's history |
| 48 | without keeping any of the deleted repository's code. |
| 49 | |
| 50 | ## Alternatives rejected |
| 51 | |
| 52 | - **Row first, storage later** (the |
| 53 | collector). The owner is told the repository is gone while its code is still |
| 54 | stored, for however long the collector takes. |
| 55 | - **Copying a request's commits into the base on creation**, like GitHub's |
| 56 | `refs/pull/N/head`. Every diff would survive, but so would the deleted |
| 57 | repository's code, and it reverses 0016. |
| 58 | - **Storage inside the row's database transaction.** A partial S3 failure rolls |
| 59 | the row back over a log that has already lost entries: a live, corrupt |
| 60 | repository instead of a dead, retryable one. |
| 61 | |
| 62 | ## Consequences |
| 63 | |
| 64 | - The tombstone itself is never removed. It holds no refs and no objects, only |
| 65 | the flag and a sequence number, and it is what stops a late push from |
| 66 | resurrecting the id. |
| 67 | - A push that crashes between uploading its entry and committing it leaves an |
| 68 | orphan, but only inside `repos/<id>/entries/`, which the purge empties |
| 69 | wholesale. With one replica, a crash during a delete takes the delete down |
| 70 | too, and its retry lists the entries again. |
| 71 | - Pushes rejected for a stale ref or contention now remove their entry too, |
| 72 | since the same rule applies to data nobody asked to keep. |
| 73 | - The purge covers this process's cache only. The API is pinned to one replica |
| 74 | in `.railway/railway.ts` for this reason; a second replica needs its caches |
| 75 | told about deletions. |