Skip to content

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

  1. Checks selfie status — skips if already approved or rejected (idempotency)
  2. Calls Rekognition IndexFaces on the selfie image in R2
  3. If a high-confidence face is detected (>= 90): sets selfie.status = 'approved', stores qualityScore
  4. 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

  1. Downloads the original from R2
  2. Sharp-resizes to 400px and 800px (fit inside, no enlargement)
  3. Strips EXIF data (GPS must never reach clients)
  4. Uploads thumb-400.jpg and thumb-800.jpg to R2
  5. 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

  1. Downloads the original from R2
  2. Builds an SVG text overlay (acme-photo-platform, 45% white opacity, rotated −30°)
  3. Composites the overlay onto the image using Sharp
  4. Strips EXIF data
  5. Uploads watermarked.jpg to R2
  6. Updates photo.watermarkedKey and sets photo.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

  1. Ensures the project's Rekognition collection exists (creates lazily)
  2. Calls Rekognition SearchFacesByImage against the project collection
  3. For each face match:
    • >= 75 confidence → inserts photoTag with reviewStatus = 'confirmed'
    • 55–74 confidence → inserts photoTag with reviewStatus = 'pending'
    • < 55 → discarded
  4. 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

  1. Checks the user has an approved selfie — skips if not
  2. Finds all projects the user is a member of
  3. For each project, finds all ready photos with no existing tag for the user
  4. Enqueues recognize_faces for each untagged photo

Idempotencyrecognize_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

  1. Re-fetches the tag — skips if it no longer exists or is not confirmed (idempotency)
  2. Fetches user name and email
  3. Fetches project name for email context
  4. 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.

Acme Photo Platform — Internal Documentation