Content Lifecycle: Edit, Delete, Hide
How posts and comments are edited, deleted and hidden, who may do what, and what happens to everything that references them.
Who may do what
One policy module, packages/types/src/content-policy.ts, is enforced by the API and used by the app only to decide which menu actions to show.
| Action | Post author | Comment author | Post author, on others' comments | Anyone else |
|---|---|---|---|---|
| Edit post | ✓ (1 hour, 5 edits) | |||
| Delete post | ✓ | |||
| Edit comment | ✓ (1 hour) | ✗ | ||
| Delete comment | ✓ | ✓ | ||
| Hide / unhide comment | ✓ |
Content the viewer can't see (deleted, blocked, someone else's unpublished post) is a 404; visible-but-not-yours is a 403. Deleted content returns code: "POST_DELETED" / "COMMENT_DELETED".
Schema
| Model | Fields |
|---|---|
Post | deletedAt, deletedById, editedAt (user edits only — updatedAt also moves on media sync), version (edit count and concurrency token), retainUntil (hold) |
Comment | deletedAt, editedAt, version, hiddenAt, hiddenById |
Media | removedAt — removed from its post by an edit |
PostRevision / CommentRevision | The body (and, for posts, media ids and link card) a later edit replaced |
ContentAuditLog | Actor, action, target, author, previous content — written in the same transaction as every edit, delete and hide |
Report | snapshot — the content as it was when reported |
Read paths filter through shared fragments: onlyPublishedPosts() (excludes deleted posts for everyone, the author included), liveMedia, countedCommentWhere, hiddenCommentWhere.
Editing
Version-checked: the client sends expectedVersion; if another device edited first the API answers 409 with the current copy and the app keeps the user's text. Previous versions are kept as revisions. Posts show "Edited" with a public, text-only history; comments show "Edited" only. Mentions are re-synced (a user is notified only if no earlier version tagged them) and the link card is re-derived. Details: Content API.
Deleting
Soft delete, then purge.
- At delete: the row gets
deletedAtand disappears from every read path (feeds, profile, search, trending, detail, comment lists, socket join). Notifications about it are deleted. Its files are made unreachable immediately by ogun'smedia-revokejob: private ACL on the original and every variant, a DigitalOcean CDN purge whenDO_API_TOKEN/DO_CDN_ENDPOINT_IDare set, and the Mux playback id deleted.post:deletedreaches open screens; the app leaves the post's screen and closes the viewer. DM shares render "This post is unavailable". - Comments: a reply, or a top-level comment without live replies, disappears. A top-level comment with live replies stays as an anonymous placeholder until its last reply goes.
- Purge (ogun
content-sweep, daily at 04:00 UTC): rows deleted more than 30 days ago are hard-deleted with everything referencing them (purgePostRows/purgeCommentRowsin@playpals/db), then their files and Mux assets. Skipped while the post has an open report orretainUntilis in the future. Media removed by an edit is purged on the same schedule.
Moderator removals (admin remove_content) soft-delete the same way with a one-year hold. Admins can hold or release any post from the report page (PUT /admin/posts/:id/retention). Account deletion purges the user's posts and comments synchronously through the same helpers.
What is kept
| Kept | For |
|---|---|
Report rows and their snapshot | Moderation history; detached from the content at purge |
ContentAuditLog | 1 year; its previous content is redacted 90 days after the content is purged |
Held posts (retainUntil) | Until the hold ends |
Hiding
See Moderation.
In the app
- Menus:
lib/content-actions.tsbuilds each menu from the policy;components/moderation/content-action-sheet.tsxdraws it everywhere. - Cache: every mutation and realtime event writes through
lib/post-cache.ts(removePostEverywhere,patchPostEverywhere,patchCommentEverywhere,markCommentDeleted,moveCommentToHidden/FromHidden). Mutations are optimistic with rollback and an error toast (lib/content-errors.ts). - Editing: the post composer (
components/composer/post-composer.tsx) has an edit mode at/post/edit/[postId]; comments are edited in the existing comment composers vialib/comment-edit-store.ts.
