Worker Jobs
All workers live in apps/media-api/src/workers/ and are registered on startup in apps/media-api/src/index.ts.
Each job type has its own BullMQ queue (named after the job). Workers only process their own queue — they never compete with each other.
Default job options (set in queue.service.ts):
- 3 attempts with exponential backoff (2s initial delay)
- Completed jobs: retain last 100
- Failed jobs: retain last 500
Job names are defined as constants in packages/config/src/jobs.ts. Never hardcode job name strings.
process_selfie
Triggered by POST /v1/selfie/upload-url after the selfie record is created.
Payload { selfieId, userId, key }
What it does
- Checks selfie status — skips if already
approvedorrejected(idempotency) - Calls Rekognition
IndexFaceson the selfie image in R2 - If a high-confidence face is detected (
>= 90): setsselfie.status = 'approved', storesqualityScore - If no face or low confidence: sets
selfie.status = 'rejected'with a reason
Idempotency — status check at the top prevents double-indexing.
generate_thumbnail
Triggered by photo upload URL request.
Payload { photoId, key }
What it does
- Downloads the original from R2
- Sharp-resizes to 400px and 800px (fit inside, no enlargement)
- Strips EXIF data (GPS must never reach clients)
- Uploads
thumb-400.jpgandthumb-800.jpgto R2 - Updates
photo.thumbnailKey
Idempotency — re-running re-uploads the same thumbnails, which is safe.
generate_watermark
Triggered by photo upload URL request.
Payload { photoId, key }
What it does
- Downloads the original from R2
- Builds an SVG text overlay (
acme-photo-platform, 45% white opacity, rotated −30°) - Composites the overlay onto the image using Sharp
- Strips EXIF data
- Uploads
watermarked.jpgto R2 - Updates
photo.watermarkedKeyand setsphoto.status = 'ready'
Idempotency — re-running re-uploads the same watermarked file, which is safe.
Container requirement
The media-api Docker image must have fontconfig and fonts-liberation installed for SVG text rendering to work.
recognize_faces
Triggered by photo upload URL request.
Payload { photoId, projectId, key }
What it does
- Ensures the project's Rekognition collection exists (creates lazily)
- Calls Rekognition
SearchFacesByImageagainst the project collection - For each face match:
>= 75confidence → insertsphotoTagwithreviewStatus = 'confirmed'55–74confidence → insertsphotoTagwithreviewStatus = 'pending'< 55→ discarded
- Raw confidence score is always stored regardless of outcome
Idempotency — duplicate (photoId, userId) inserts are caught by the unique index and skipped.
WARNING
Pending tags are never surfaced to end users. Only confirmed tags are visible.
retrigger_matching
Triggered manually (e.g. after a user uploads a new selfie or an operator requests re-processing).
Payload { userId }
What it does
- Checks the user has an approved selfie — skips if not
- Finds all projects the user is a member of
- For each project, finds all
readyphotos with no existing tag for the user - Enqueues
recognize_facesfor each untagged photo
Idempotency — recognize_faces is idempotent; re-enqueueing is safe.
notify_user_tagged
Triggered by tag confirmation (when a pending tag is confirmed via manual review, or when recognize_faces creates a confirmed tag directly).
Payload { photoTagId, userId, photoId }
What it does
- Re-fetches the tag — skips if it no longer exists or is not
confirmed(idempotency) - Fetches user name and email
- Fetches project name for email context
- Sends a transactional email via Resend
Idempotency — tag status check prevents sending for non-confirmed tags.
TIP
If RESEND_API_KEY is not configured, this worker skips gracefully with a log message.