Where files live
Uploaded files — gym media, contract assets, member progress photos, maintenance photos and videos, SAR packages and the rest — are held in storage buckets rather than in the database. Buckets are either public (served straight to a website) or private (reached only through a short-lived signed link).
Nightly backup to Cloudflare R2
The GitHub workflow .github/workflows/backup-storage.yml runs daily at 03:00 UTC and mirrors the configured buckets into Cloudflare R2. Fifteen buckets are covered, including the private sar-packages bucket.
It is a mirror, not an archive: when a file is deleted at source — such as a SAR pack purged after thirty days — the deletion carries across on the next run. That is intentional for anything with a retention rule, and worth remembering before you rely on R2 to recover something a gym deleted a week ago.
Adding a new bucket
- Create the bucket in every environment. A bucket that exists in development but not production produces a 404 "Bucket not found" the first time the feature is used for real.
- Set it private unless the files are genuinely meant to be public.
- Add it to the bucket list in the backup workflow, otherwise it is silently unprotected.
- Where a one-off creation script exists in
docs/, use it — the scripts are written to be safe to re-run.
Common symptoms
| Symptom | Usual cause |
|---|---|
| "Bucket not found" on an action that works in development | The bucket was never created in that environment |
| Download link works then stops | Signed links are deliberately short-lived; generate a fresh one |
| Upload rejected | Per-bucket size limit or an unsupported file type |