1 · Lifecycle
States (pending → ready → deleting), the functions that drive
transitions, and the invariants each enforces.
Read →
Every user-uploaded file in M2 Planner — avatars, logos, project photos,
moodboard images, inspection PDFs, product images, partner certifications,
CMS content — lives in one Cloudflare R2 bucket and is tracked by one
Postgres table: assets (apps/api/src/db/schema.ts:6636).
This section documents the entire lifecycle end-to-end, with every claim
cited to path:line in the codebase, so you can verify the system yourself
rather than trust the prose.
assets.refcount > 0 protects a file.
There is no secondary “is it used?” signal to get out of sync with.
(apps/api/src/db/schema.ts:6636-6675)attachFile / detachFile.
Both run inside the same DB transaction as the domain mutation, so
refcount and FK can’t drift. (apps/api/src/services/files.ts:202,
:219)deleting when refcount hits 0. Transition is
atomic — a single UPDATE with a CASE expression guarantees it.
(files.ts:223-231)purgeFile refuses to delete any row not
in deleting. (files.ts:288)apps/api/src/queues/cleanup.ts:181-275)For a bug to delete an in-use file, all of these would have to fail at once: the refcount update, the status CASE expression, the purge status check, and five cleanup gates. The Risk Register lists every scenario that could plausibly erode those gates and the mitigations for each.
flowchart LR U[Client app<br/>web / mobile / admin] -- "POST /api/upload" --> API[API Worker] API -- "INSERT assets<br/>status=pending" --> PG[(Postgres)] API -- "signed URL" --> U U -- "PUT file" --> R2[(R2 bucket)] U -- "POST /api/upload/:id/commit" --> API API -- "UPDATE status=ready" --> PG API -- "attachFile on domain mutation" --> PG API -- "detachFile on delete" --> PG CRON[Cron 02:00 UTC] --> QUEUE[CLEANUP_QUEUE] QUEUE --> CLEAN[cleanupFiles<br/>+ cleanOrphanedFiles] CLEAN -- "delete" --> R2 CLEAN -- "delete" --> PG1 · Lifecycle
States (pending → ready → deleting), the functions that drive
transitions, and the invariants each enforces.
Read →
2 · Cleanup Safety
Five-layer defense in depth for cleanOrphanedFiles + two-phase
cleanupFiles. The “can it delete an in-use file?” walkthrough.
Read →
3 · Consumers
Every feature that attaches files to domain rows (user-avatar, portfolio photos, moodboard board_state, inspection PDFs, …) with cited attach/detach sites. Read →
4 · Risk Register
Eight ranked ways the guarantees could erode, with likelihood, impact, and concrete mitigations. Read →
5 · Verification
Copy-paste psql / grep / vitest commands that prove each claim
on the live system right now.
Read →