Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesUploads R2Uploads & R2 — Data Model

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:

TableR2 columnNotes
progress_photosr2_key, thumbnail_r2_keyThumbnail key set by a background job after upload.
exercise_comments (workout_results / messages variants)r2_key, mime_type, file_sizeImage and video MIME types both allowed.
messagesattachment_r2_key, attachment_thumbnail_r2_keyImage attachments only.
organizationslogo_urlStores either an R2 key or a fully-qualified URL; presign happens at read time.
class_sessionscover_image_r2_keyOptional cover.
form_signaturesr2_keyLives in the compliance bucket.
export_jobsfile_keyexports/<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

KeyTTLPayload
upload:<uploadId>900s{ r2Key, mimeType, fileSize, userId }
r2:geturl:<bucket>:<key>:<expiresIn>expiresIn - 300sPresigned GET URL string

R2 buckets

Bucket envUse
R2_BUCKET_NAMEDefault. Holds all application media, exports, logos.
R2_COMPLIANCE_BUCKET_NAMESigned 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.