The trainee app uploads a file once and then passes the returned URL to whichever endpoint needs it: a progress photo, an avatar, a meal photo, a technique video or a form answer. Mount: /v1/trainee/uploads, router traineeUploadsRouter in src/modules/uploads/uploads.routes.ts, service createUploadsService in uploads.service.ts. Lane: trainee. The router mounts authenticateTrainee only, without the app access guard. A preview token is refused because the only route is a POST. See Authentication.

Endpoints

POST /v1/trainee/uploads

Stores one file and returns its public URL. Auth: trainee token. The body is the raw file bytes. It is not multipart and not base64.
string
required
The file’s media type, for example image/jpeg or video/mp4. See the size note below for why this should not be application/octet-stream or application/json.
string
default:"application/octet-stream"
The media type to store the object with. The app sends the same value as Content-Type.
string
default:"upload"
The original file name. Only its extension is used.

Size limits

The route’s own parser is express.raw with type: () => true and a limit of 15 MB. Two global parsers in app.ts run before it and win for their content types: A body over the limit returns status 413 with code BAD_REQUEST and message payload too large. The server does not check the media type against an allow-list and does not inspect the bytes, apart from HEIC detection.

What the service does

  1. Requires R2 storage to be configured.
  2. Detects HEIC images from the file bytes and converts them to JPEG, because coach browsers cannot display HEIC. If the conversion fails, the original is stored and a warning is logged.
  3. Picks the extension: jpg after a HEIC conversion. Otherwise the extension of x-file-name when it is 1 to 8 lower-case letters or digits. Otherwise from the media type: jpg, png, webp, gif, mp4 or mov. Otherwise bin.
  4. Stores the object under media/<uuid>.<ext>, prefixed with R2_PATH_PREFIX when set, with the media type as its content type.
  5. Returns the public URL built from R2_PUBLIC_BASE_URL and the key.
Storage is Cloudflare R2 through the S3 API. The env vars are R2_ENDPOINT, R2_REGION, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET_NAME, R2_PUBLIC_BASE_URL and R2_PATH_PREFIX. Uploads are not tied to the trainee in the database. No row is written until the URL is used by another endpoint, and unused objects are not cleaned up by this module. Response: 200.
Errors:

Where the URL goes next