Skip to content

feat: delete MediaLit assets when their resource is deleted (#157) - #162

Merged
clbotdev merged 3 commits into
mainfrom
feat/157-medialit-cleanup
Oct 29, 2026
Merged

clbotdev merged 3 commits into
mainfrom
feat/157-medialit-cleanup

Conversation

@clbotdev

Copy link
Copy Markdown
Contributor

Closes #157 (part of #152).

What

  • Shared cleanup helper deleteOrphanedMedia(teamId, mediaIds) (apps/api/src/media/queries.ts). deleteMediaReferencesForResource now returns the media rows it released. Once the resource is deleted, each released row that has no references left and that no stored sent content (email_content_versions) shows is deleted, along with its MediaLit file.
  • Wired into template delete, sequence email delete and sequence/broadcast delete. A delete that covers several emails cleans up the media of every email.
  • Sent mail is never broken. A sequence/broadcast email with a delivery or a content version keeps its images. A template that a transactional email was sent with keeps its images. Those images stay in the Media library with no references, where the user can delete them.
  • Team isolation: a MediaLit file is deleted only when no media row for it is left in any team.
  • Retry: a new media-deletion BullMQ queue (src/media/deletion-queue.ts + worker, same pattern as the contact deletion queue; shut down in index.ts). The row is deleted first. A failed MediaLit delete goes to captureError (medialit_delete_failed) and is queued: first try after 1 minute, then exponential backoff over 10 attempts (~17h). The worker skips a file that is in use again.
  • Team purge (deleteTeamMediaFiles, used by org delete): it now deletes all rows and queues the files MediaLit fails to delete. Before, it kept the failed rows and never retried them.
  • template_deleted, sequence_deleted and sequence_email_deleted carry media_deleted_count (allowlisted).

Decisions (ADR 0006)

  • Editing content doesn't delete media. The removed image may be in sent versions or be added back. The Media library is where files get deleted (same as Mailchimp and Klaviyo content libraries). No grace period: drafts were never seen by anyone, and sent mail is kept outright.
  • Archiving a team doesn't delete media, because archived teams can be restored. Org deletion (and later account deletion, Delete account #134) purges.
  • No one-off cleanup script for media that is already orphaned. It's optional and not included.

Docs

OpenAPI descriptions for the three delete routes, MCP tool descriptions, apps/api/docs/media-library.md, user docs workspace/media.mdx, ADR 0006.

Tests

Real PGlite DB with MediaLit service.ts mocked: template delete (own media deleted; media shared with another template kept; transactional-sent template kept; another team's row for the same file is untouched), sequence email delete (shared kept), sequence delete across 3 emails (bulk), draft broadcast deleted vs sent broadcast kept, a draft email sharing an image with a sent email, deleteOrphanedMedia (references, sent-content check, other team's ids ignored), MediaLit failure → row gone, job queued and captureError called, then the retry deletes the file. Org delete: multi-team purge with other orgs untouched, plus the failure → queued case. Results: API 902+ pass / 0 fail, web and contract tests pass, lint, typecheck, check:drift and test:conformance all green.

E2E (browser, real stack)

Isolated API + web (next dev) + Postgres + Redis, with a local MediaLit stub that implements TUS upload, seal, delete and image serving. Its delete answers 503 while a flag file exists.

  • Two images were really uploaded through the dashboard editor (image block → Browse → Upload over TUS to the stub) and sealed when the template was saved. Other images were put into content through the REST API (they are still sealed through the real path).
  • Deleted template A in the dashboard → stub got exactly DELETE lit…(A).
  • Deleted template B in the dashboard (its image is shared with template C and a sequence email) → no delete; row kept.
  • Deleted sequence email 1 on the sequence page → exactly DELETE seqone.
  • Deleted the sequence, the draft broadcast and a "sent" broadcast through the REST API. The dashboard has no sequence or broadcast delete yet (Delete broadcasts (with archive for sent ones) #147/Delete sequences #148). Results: DELETE seqtwo only (the shared image was kept); DELETE draftbcast; the sent broadcast's image was kept. The "sent" state was set by inserting an email_content_versions row (what a send writes). No real send happened.
  • MediaLit outage: deleted template C in the dashboard while the stub returned 503. The template and its media row are gone and the stub logged one 503. After MediaLit recovered, the queued job retried about 60s later and the delete succeeded. Final media: only the sent broadcast's images.
  • Not checked in e2e: PostHog delivery of captureError (no PostHog key locally; unit tests check the call).

)

Template, sequence and sequence email deletes now delete the media rows
they leave without references, and their MediaLit files. Media shown by
sent mail is kept. Failed MediaLit deletes are queued for retry on a new
media-deletion BullMQ queue and sent to captureError; team purges use
the same path.
@clbotdev
clbotdev merged commit cbb2566 into main Oct 29, 2026
5 checks passed
@clbotdev
clbotdev deleted the feat/157-medialit-cleanup branch October 29, 2026 10:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Delete MediaLit assets when their resource is deleted

2 participants