Uploads & R2 — Data Model
Postgres
No dedicated upload table — staged uploads live entirely in Redis. Parent rows persist the R2 key alongside their own state:
| Table | R2 column | Notes |
|---|---|---|
progress_photos | r2_key, thumbnail_r2_key | Thumbnail key set by a background job after upload. |
exercise_comments (workout_results / messages variants) | r2_key, mime_type, file_size | Image and video MIME types both allowed. |
messages | attachment_r2_key, attachment_thumbnail_r2_key | Image attachments only. |
organizations | logo_url | Stores either an R2 key or a fully-qualified URL; presign happens at read time. |
class_sessions | cover_image_r2_key | Optional cover. |
form_signatures | r2_key | Lives in the compliance bucket. |
export_jobs | file_key | exports/<orgId>/<jobId>.csv. |
R2 keys follow the namespaced shape <prefix>/<userId>/<uploadId>.<ext> for staged uploads. Server-managed paths (exports/..., org-logos/...) use their own prefix.
Redis
| Key | TTL | Payload |
|---|---|---|
upload:<uploadId> | 900s | { r2Key, mimeType, fileSize, userId } |
r2:geturl:<bucket>:<key>:<expiresIn> | expiresIn - 300s | Presigned GET URL string |
R2 buckets
| Bucket env | Use |
|---|---|
R2_BUCKET_NAME | Default. Holds all application media, exports, logos. |
R2_COMPLIANCE_BUCKET_NAME | Signed compliance PDFs (FIT-158). 7-year immutable retention, bucket-level. |
Lifecycle
- Staged uploads not consumed within 15 min → Redis key expires; R2 object remains until manually swept.
- Parent rows that hold an R2 key but are soft-deleted: the R2 object persists until a hard delete (no GC sweeper today).
- Export CSVs: persisted alongside
export_jobs; no automatic expiry — admin sweep when needed. - Compliance bucket: 7-year retention enforced by bucket policy.