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. It calls the Retina clustering service to generate a 512-dim face embedding and, if approved, enqueues cluster_faces for each project the user has joined.
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 800px; after completion enqueuescluster_facesfor each project member with an approved selfiegenerate_watermark— Sharp composite with text overlay, EXIF stripped
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
Photographer Photos
GET /v1/photographers/me/projects/:projectId/photos
List photos for a project the authenticated photographer is assigned to. Photos are ordered by takenAt descending (undated last).
Audience backoffice · Role photographer
Response 200 — array of photo objects including takenAt (nullable ISO 8601), status, thumbnailUrl, fullUrl.
DELETE /v1/photographers/me/projects/:projectId/photos/:photoId
Delete a photo the authenticated photographer uploaded. Removes the DB row and all R2 objects (original, thumbnails, watermarked).
Audience backoffice · Role photographer · Response 204
Operator Media
GET /v1/operators/me/photos
List all photos across the operator's projects, with optional multi-select filtering.
Audience backoffice · Role operator
Query params
projectIds— comma-separated list of project UUIDs (optional, defaults to all)photographerIds— comma-separated list of photographer UUIDs (optional, defaults to all)
Response 200 — array of photo objects including projectId, projectName, photographerName, takenAt (nullable ISO 8601).
GET /v1/operators/me/projects/:projectId/photos
List photos for a single project belonging to the authenticated operator.
Audience backoffice · Role operator
Response 200 — array of photo objects including photographerName and takenAt.
DELETE /v1/operators/me/projects/:projectId/photos/:photoId
Delete any photo within the operator's project. Removes the DB row and all R2 objects.
Audience backoffice · Role operator · Response 204
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.