Skip to content
FeaturesPricingAffiliateBlogHelpAboutContact
Get StartedSign In
Back to Blog
industry2026-05-2412 min read

I deleted 100 summer products but my R2 storage bill stayed the same — product DELETE image cleanup (PR #603 BBB F5)

Fabrizio Naples Spaccanapoli Via dei Tribunali 42-yo 18-yr Pizzeria di Borgo San Lorenzo 50-cover Neapolitan pizzeria thMenu Pro 30 months. Naples 2 culinary seasons summer tomato + basil + buffalo-mozzarella winter white-truffle + porcini + braised-meat 60 product rotation × 2 images/year × 4 years 960 images ghost ~340MB. October rotation 62 summer items removed glances R2 widget 7.1GB/10GB Pro never dropping. Math 240 images/year delete × 4 years 960 images R2 unreachable each 350KB ~340MB ghost. Support ticket Pro 10GB delete 240 images/year storage never drops bug. 3 wrong theories (1) soft-delete residue products deleted_at + product_images orphan SELECT COUNT 2420 orphan + 1033 deleted products; (2) R2 lifecycle expire R2 no automatic expiry list menu-images/<fabrizio_id>/* 3891 R2 objects active row 1471 → 2420 orphan; (3) DELETE handler R2 untouched correct. Forensic apps/web-admin/src/app/api/products/[id]/route.ts DELETE UPDATE products SET deleted_at=? only soft + R2 untouched + product_images orphaned no FK cascade. apps/web-admin/src/app/api/menus/[id]/route.ts DELETE menu hard-delete FK-cascade products + product_images but R2 still untouched. Both paths R2 binary orphan. URL knower deleted product image still load GDPR Art.17 right-to-be-forgotten incomplete. PR #603 batch BBB F5 3-layer fix Layer 1 product DELETE SELECT image URLs + canonical R2 key regex r2KeyRegex=/^https?:\/\/[^/]+\/(menu-images\/[a-f0-9-]{36}\/[A-Za-z0-9._-]+)$/ strict path traversal + cross-tenant prefix bypass + arbitrary URL reject. Layer 2 cross-tenant prefix guard extracted key restaurant_id segment session restaurant_id match filter k.includes menu-images/${userRestaurantId}/ tenant isolation. Layer 3 best-effort MENU_IMAGES.delete(key) per key R2 throw silent swallow audit log r2_objects_deleted: 12 / r2_objects_attempted: 14. DB cleanup product DELETE FROM product_images WHERE product_id=? menu FK cascade. Production audit N-day all restaurants orphan R2 object 184712 ~71GB. Pro tier total quota 10GB × 247 Pro+ subs = 2470GB orphans ~3%. Backfill cron r2-orphan-cleanup one-off manual paginated per-restaurant R2 list + product_images row check + orphan delete 6 hours. Fabrizio account 2420 orphan delete 7.1GB → 3.4GB email PR #603 BBB F5 shipped retro-backfill complete 52% drop 1-month Pro credit Grazie dashboard widget honest. Audit log r2_objects_deleted operator dashboard Last month: N objects removed from R2 widget. Burhan Antalya Kaleici Antalya Sis Kebap+Tarcinli Kunefe 50-cover Mediterranean 14-yr same pattern 5 yr rotations storage 8.4GB → 4.2GB parallel ticket. Pattern DELETE not only DB row but binary asset + audit log triple canonical soft-delete + dont-touch-R2 silent billing waste + silent GDPR Art.17 violation hard-delete cascade product_images + R2 stranded same problem. Sibling sweep customer profile photo (PR #611 DDD F1 erase-user) + brand logo (PR #661 XI F3) + menu cover image + restaurant logo (PR #603 BBB F5) + backup snapshots separate cron. Implementation DELETE handler binary asset URL SELECT + canonical R2 key strict regex extract + cross-tenant prefix guard + best-effort R2.delete try/catch + count attempted/deleted + DB cleanup + audit log + operator dashboard widget + quarterly orphan sweep cron + align GDPR Art.17 erase-user path. PR #603 reference.

th

thMenu Team

thmenu.com

Found this helpful? Share it.