Uploads & R2
Two-stage upload pipeline that fronts Cloudflare R2 for every user-supplied binary in FitKit — progress photos, exercise comment media, message attachments, organization logos, signed compliance PDFs, and tokenized form signatures.
What
r2.service.ts— thin S3-flavoured client over Cloudflare R2 with bucket-aware reads, presigned URL minting, and a Redis-cached presigned-GET helper.uploads.service.ts+uploads.controller.ts— the “generic” presigned upload pipeline used for any owner type that follows the canonical stage-then-link pattern (exercise_comment,progress_photo, plus an internalmessagepolicy).- Domain-specific upload paths inside
progress-photos,workouts,messages,forms,organizations, andclass-sessionsall funnel throughR2Servicefor object storage, even when they bypass the staged-upload Redis flow.
Why
- R2’s S3-compatible API is cheap, geographically distributed, and Cloudflare-hosted alongside the platform’s other infra.
- Direct-to-R2 presigned URLs eliminate proxying large files through the API host.
- A Redis-staged design lets us atomically link an upload to its parent on the writing transaction, preventing dangling objects and avoiding “orphan after auth failure” cleanup work.
- Per-owner policy tables (MIME + size) keep media constraints out of the leaf services.
Who
- Members — upload progress photos, exercise comment media, message images, completed form signatures.
- Coaches — upload media on exercise comments, messages.
- Owners — upload organization logos, minisite hero / gallery images.
- Anonymous (token-gated) — submit tokenized form signatures from a mobile signing flow without a Clerk session.
Persona impact
| Persona | Impact |
|---|---|
| Member | Smooth media uploads with size limits enforced before the client wastes bandwidth. |
| Coach | Reliable media attached to coaching threads without proxying through the API. |
| Owner | Org branding (logo, minisite hero) hosted at the CDN edge for sub-second LCP. |
| Compliance signatory | Tokenized signature upload from a phone, no signup required. |
Capabilities
- Presigned PUT URL minting (10-min TTL by default).
- Per-owner MIME + size policy enforcement at presign time.
- Two-stage flow: presign → client uploads → parent write
consumeManydeletes the Redis staging key. - Presigned GET URL minting with optional Redis cache (TTL =
expiresIn - 300s) keyed by(bucket, key, expiresIn). - Dedicated compliance bucket (
R2_COMPLIANCE_BUCKET_NAME) for FIT-158 signed PDFs to enable bucket-level retention policies. - Token-gated public PUT for tokenized form signatures (no Clerk auth required).
Related features
progress-photos— owner typeprogress_photo; thumbnails generated server-side after upload.exercises/workout-results— owner typeexercise_comment; image + video allowed.messages-comments—messagepolicy (images only, no video to keep thread bandwidth low).forms(FIT-158 signature uploads) — usesR2Servicedirectly with the compliance bucket.organizations— logo upload usesgetPresignedUploadUrldirectly, no staging.minisites— references R2 objects published from the dashboard editor.
Status
Shipped. The staged-upload pipeline is the canonical pattern for new owner types.
Gaps
- No background cleanup for Redis staging keys that expire without being consumed. The R2 object becomes orphaned. Acceptable for now (low volume); a sweeper job is on the backlog.
- No virus scanning (ClamAV / S3 ObjectLambda). Risk surface: small org-scoped media accepted from authenticated members.
- No content-type sniffing — we trust the client
Content-Type. A malicious upload could spoofimage/pngfor a binary; downstream consumers must not execute uploaded files (they don’t today). - No per-org storage quota — heavy uploaders are not throttled today. Tracked as future work alongside platform-billing.
- Compliance bucket lifecycle policy (7-year immutability) must be set bucket-side; the code uses the bucket but doesn’t manage retention.