3.7 KiB
3.7 KiB
12 — Soft Deletes, Retention & Culling
Deletes in SoloPM are soft (deleted_at) so mistakes are recoverable. The culling job is the only code path that hard-deletes, and it is responsible for cascades and disk files — which is why DB-level ON DELETE CASCADE is deliberately absent.
Lifecycle
- Soft-deleted rows are invisible to every normal query (
deleted_at IS NULLfilters, 01-architecture.md) and leave the search index immediately (10-search.md) - Rows older than
CULL_AFTER_DAYS(default 30) sincedeleted_atare hard-deleted by the cull job - Restore before cull = clearing
deleted_at(no UI in v1; document the SQL in ops notes)
Cull job
- Runs as a
culljob scheduled daily (03-domain-events.md) + CLIsolopm cull [--dry-run]+POST /admin/cull?dry_run= - Dry-run mode: full traversal, returns/logs the would-delete report
{resource_type: count}+ file list, deletes nothing — required before trusting the job - Deletion order respects FK dependencies (children first)
- Cascade scope per culled resource:
- project → its epics, issues (and their cascade), labels, members, favorites, dependencies (both directions), wiki pages, links, attachments (+files), webhooks + deliveries, receipts
- issue → its comments (and their cascade), issue_labels, watchers, blocker edges (both directions), sub-issue links (children's
parent_issue_id→ NULL), links, attachments (+files) - epic → member issues'
epic_id/epic_rank→ NULL (issues survive) - comment → its links, attachments (+files)
- label → its issue_labels pivots
- wiki page → row only
- attachment → row and the file on disk — file deletion and row deletion happen together; a failed file unlink (other than not-exists) aborts that row's cull and is logged
- User culling (soft-deleted users past retention):
- Reassign authored content to the zombie user (id 2): issues.creator_id/assignee_id, comments.author_id, epics.creator_id, labels.created_by, attachments.uploader_id, links.created_by, wiki created_by/updated_by, project creator_id
- Precondition (already enforced at delete time): not sole owner of any live project
- Delete their favorites, watches, notifications, oauth_accounts, api_tokens, sessions
audit_log.initiated_bykeeps the original user id — audit is immutable; UI renders unknown ids as "Deleted User"
- Non-soft-delete retention in the same job:
notificationsolder than 90 days → hard deleteeventsprocessed > 30 days ago → hard deletejobsdone/dead > 30 days ago → hard deleteincoming_webhook_receipts> 90 days → hard deleteoutgoing_webhook_deliveries> 90 days → hard deleteaudit_log→ kept forever in v1 (17-decisions.md)- expired
sessions(scs handles) and revoked/expiredapi_tokens> 30 days → hard delete
- The job logs a summary line per run (counts per resource type, duration, dry-run flag) and records a
culljob row with the report inpayload
Safety checklist
- Dry-run output reviewed before first real run in any environment
- File deletions restricted to paths under
UPLOAD_DIRmatching the stored relative path (no traversal) - Whole-project cull wrapped in one transaction per project (files unlinked after commit; unlink failures logged for manual sweep)
- Integration tests: cull a project → zero orphan rows (assert per table) and zero orphan files; cull a user → zombie owns their content, audit rows untouched