Media API
Base URL: http://localhost:3003 (dev) · port 8003 (Docker)
Two JWT audiences are accepted depending on the caller:
media— end users (selfie upload)backoffice— operators and photographers (profile pictures, photo uploads)
All file uploads use presigned PUT URLs — the server never proxies file bytes. The client receives a URL and PUTs the file directly to Cloudflare R2.
Selfie Upload
POST /v1/selfie/upload-url
Request a presigned PUT URL to upload a selfie.
Audience media · Role user
Requires consent
This endpoint is guarded by requireConsent('biometric_processing'). The request is rejected with 403 if the user has no active consent row.
Response 201
{
"selfieId": "uuid",
"uploadUrl": "string",
"key": "string",
"expiresInSeconds": 600
}After uploading to uploadUrl, the process_selfie job is enqueued automatically, which indexes the face to AWS Rekognition.
Photo Upload
POST /v1/photos/upload-url
Request a presigned PUT URL to upload a project photo.
Audience media · Role photographer
Body { "projectId": "uuid" }
Response 201
{
"photoId": "uuid",
"uploadUrl": "string",
"key": "string",
"expiresInSeconds": 600
}On upload the following jobs are enqueued:
generate_thumbnail— Sharp resize to 400px and 800pxgenerate_watermark— Sharp composite with text overlay, EXIF strippedrecognize_faces— Rekognition face search against the project collection
POST /v1/photographers/me/photos/upload-url
Same as above but accepts a backoffice-audience token. Used by the backoffice-web photographer dashboard.
Audience backoffice · Role photographer
Operator Media
POST /v1/operators/me/profile-picture/upload-url
Request a presigned PUT URL for an operator profile picture.
Audience backoffice · Role operator
Response 200
{ "uploadUrl": "string", "fileUrl": "string" }fileUrl is the permanent R2 path to store on the operator profile after upload.
Storage Key Conventions
selfies/{userId}/{timestamp}.jpg
photos/{projectId}/{photoId}/original.jpg
photos/{projectId}/{photoId}/thumb-400.jpg
photos/{projectId}/{photoId}/thumb-800.jpg
photos/{projectId}/{photoId}/watermarked.jpg
operators/{operatorId}/profile.jpgNever return public R2 URLs
All download URLs served to clients must be presigned GET URLs (15-minute expiry). The R2_PUBLIC_URL is used for internal key construction only.