| `@cio/api` services | Validate input, write domain rows, then enqueue jobs via `enqueueTransactionalEmail` / `enqueueRawEmail` / `startMediaJob`. Returns to the client immediately. |
| Redis (BullMQ) | Persistent job queue. Tracks state (`waiting`, `active`, `completed`, `failed`), retries with exponential backoff, retention. |
| `media` | `probe-metadata`, `generate-thumbnail`, `extract-audio` | `startMediaJob` after a video asset upload |
| `media-transcribe` | `transcribe-audio` | Same flow; only enqueued when `OPENAI_API_KEY` is set |
| `maintenance` | `retention-compact`, `dead-letter-cleanup` | Repeatable scheduled jobs |
### Persisted state (Postgres)
The worker writes to three tables:
-`media_job` — user-visible processing state (queued → probing → thumbnailing → extracting → completed). Surfaces as the upload progress bar in the dashboard.
-`job_step` — per-step idempotency ledger so retries skip already-finished work (e.g. don't re-upload a thumbnail).
-`dead_letter_job` — terminal failures after BullMQ exhausts retries. Used for operator triage.
After a video upload, transcription usually runs as part of the media pipeline. You can **enqueue only extract-audio → Whisper** for an existing S3-backed video asset:
```bash
# Replace <org header> and Bearer token with a normal member session for the org.
curl -X POST "http://localhost:8787/jobs/media/asset/<assetId>/transcribe"\
| Worker stopped, Redis up | API enqueues normally, returns success | Sits in `waiting`. Drains as soon as the worker starts. |
| Worker + Redis both down | Email helper logs once, returns empty result; media falls back to a `media_job` row with no BullMQ id | Domain action still succeeds; work is dropped (email) or stuck in `queued` (media). |
| Worker crashes mid-job | Other workers / next worker boot picks up retry | BullMQ moves the job back to `waiting` after the visibility timeout. |
| Job exhausts all retries | API unaffected | Row in `dead_letter_job` for triage. |
## Production
Run the worker as its own service alongside the API. They scale independently