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 the Retina clustering service with the selfie image
  3. If face detection score >= 0.9: sets selfie.status = 'approved', stores the 512-dim embedding and quality score
  4. If no face or low score: sets selfie.status = 'rejected' with a reason
  5. On approval: enqueues cluster_faces for each project the user is a member of

Idempotency — status check at the top prevents reprocessing.


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
  6. Enqueues cluster_faces for every project member with an approved selfie

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 (Wairo, 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.


cluster_faces

Triggered by

  • generate_thumbnail completion — for each project member with an approved selfie
  • process_selfie approval — for each project the user is enrolled in
  • POST /v1/qr/join — when a user joins a project

Payload { userId, projectId }

What it does

  1. Checks the user has an approved selfie — skips gracefully if not
  2. Fetches all ready project photos with a thumbnailKey
  3. Generates fresh presigned GET URLs for the selfie and each thumbnail (avoids expiry issues)
  4. Calls the Retina service in chunks of 50 images, with the selfie included in every chunk as __selfie__
  5. Collects the selfie's cluster_id values across all chunks
  6. For each photo face that shares a cluster ID with the selfie, inserts a photoTag with reviewStatus = 'confirmed'
  7. New inserts trigger a notify_user_tagged job

Idempotency — duplicate (photoId, userId) inserts are silently skipped by the unique index (onConflictDoNothing); notify_user_tagged is only enqueued for genuinely new inserts.


notify_user_tagged

Triggered by cluster_faces when a new confirmed tag is created.

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.


notify_user_invited

Triggered by POST /v1/projects/:projectId/invites/bulk — one job per new invite after the DB insert commits.

Payload { inviteId, email, projectId, token }

What it does

  1. Re-fetches the invite row — skips if it no longer exists or status ≠ 'pending' (idempotency)
  2. Fetches project name for email context
  3. Builds the invite link: ${MAIN_WEB_URL}/register?invite=<token>
  4. Sends a transactional email via Resend with the join link

Idempotency — invite status check prevents duplicate sends if the job is retried.

TIP

If RESEND_API_KEY is not configured, this worker skips gracefully with a log message. If MAIN_WEB_URL is not set, the link falls back to a relative path.

Wairo — Internal Documentation