Files
classroomio/apps/jobs
Abdulmuizz Hamzat 5e2dcf71f4 docs: add dev setup guide and make package scripts cross-platform (Windows) (#664)
* chore: update README with environment setup instructions and demo account credentials

fix: replace rm -rf with rimraf in build scripts for cross-platform compatibility

fix: add shell option to pnpm command in db-setup script for Windows compatibility

chore: update package.json files to use rimraf instead of rm -rf for consistency

chore: add rimraf dependency to pnpm-lock.yaml

feat: add postinstall script to install lefthook git hooks in a cross-platform manner

* docs(qa): add QA test plan, flow checklists, and bug-report workflow

Add a repo-native QA execution layer on top of FEATURE_AUDIT.md (the
feature map). Test cases live in the repo; bugs live in GitHub Issues, so
there is no duplicate tracker to keep in sync.

QA system:
- FEATURE_AUDIT.md: the feature map - every feature, its user flow, and
  edge cases with file:line citations.
- qa/README.md: the workflow, severity/difficulty rubrics, label setup,
  and a static feature -> flow coverage map proving all 42 audited
  features have a test plan.
- qa/flows/01..13: per-flow happy-path checklists covering signup &
  onboarding, course authoring, people/invites/roles, student ->
  certificate, AI assistant & tutor, programs/attendance, community,
  org public site, media & import, org settings, public API/MCP,
  enterprise SSO/licensing, and billing & AI credits.
- .github/ISSUE_TEMPLATE/qa-bug.yml: a QA Bug template requiring
  severity, difficulty, area, repro steps, and a screen recording.

Dev tooling:
- package.json: add db:studio and docker:build scripts.
- packages/db: add db:studio (drizzle-kit studio); fix db:reset on
  Windows by spawning the pnpm.cmd shim with shell:true (Node's
  CVE-2024-27980 fix blocks spawning .cmd/.bat without a shell).

* chore: update QA bug template and README for clarity and improved workflow

* docs: enhance DEV_SETUP_NOTES and README for clarity on prerequisites and email setup
chore: update package.json scripts for cross-platform compatibility
feat: add cross-platform Prettier check script for pre-commit hook

* Remove QA documentation and flow files for ClassroomIO

- Deleted the entire QA README.md file which contained the QA process, bug filing guidelines, and feature coverage map.
- Removed all flow files (01 to 13) that detailed the QA testing processes for various features including admin signup, course authoring, people management, student learning, AI assistant, programs, community, org public site, media management, org settings, public API, enterprise SSO, billing, and AI credits.

* fix: update postinstall script
2026-06-15 17:40:06 +01:00
..

@cio/jobs-worker

BullMQ worker process for ClassroomIO. Runs every background job the @cio/api enqueues — emails, lesson video post-processing (probe / thumbnail / extract-audio / transcribe), and maintenance tasks.

The worker is a separate Node process from the API. They communicate only through Redis. Stop the worker, restart the worker, redeploy the worker — the API keeps serving requests; queued work just waits in Redis until a worker picks it up.

How it fits together

┌──────────┐  enqueue(payload)   ┌───────┐  pop job   ┌─────────────────┐
│  @cio/api │ ──────────────────▶ │ Redis │ ─────────▶ │ @cio/jobs-worker │
└──────────┘                     └───────┘            └─────────────────┘
      ▲                                                        │
      │ HTTP                                          provider │
      │                                                  call ▼
   browser                                            ┌─────────────────┐
                                                     │ SMTP / S3 / etc │
                                                     └─────────────────┘
Layer Responsibility
@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.
@cio/jobs-worker (this app) Pops jobs, calls the provider (@cio/email, ffmpeg, OpenAI, …), updates media_job rows where applicable, dead-letters terminal failures.

Heads-up: the API still sends a few emails directly

Auth-flow emails are sent synchronously by @cio/api — not queued. They're the four templates Better Auth needs in-line during sign-up / password recovery (the user is waiting for a link to arrive in their inbox):

  • forgotPassword
  • onPasswordReset
  • verifyEmail (sign-up + email-change)

So both processes need SMTP credentials. They can point at the same SMTP server. Everything else (welcome, invites, newsfeed, course completion, payment requests, the public /mail route, submission notifications) goes through this worker.

Queues

Queue Jobs Where the payload comes from
emails send enqueueTransactionalEmail / enqueueRawEmail in apps/api/src/services/jobs/email-jobs.ts
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.

Email jobs intentionally don't have a parallel DB ledger - BullMQ's own job state (visible via bull-board, see below) is the source of truth for "did this email send?", and dead_letter_job captures final failures.

Running locally

Prerequisites

  1. Redis on localhost:6379 (e.g. docker run --rm -d -p 6379:6379 redis:7-alpine)
  2. Postgres reachable via DATABASE_URL
  3. ffmpeg + ffprobe on $PATH (only required for the media worker — emails and maintenance workers boot fine without them; the worker logs a warning at startup if missing and media jobs will fail until it's installed)
    • macOS: brew install ffmpeg
    • Debian/Ubuntu: apt install ffmpeg
    • Override with FFMPEG_PATH / FFPROBE_PATH for non-standard locations.

Env

The worker reuses most of apps/api/.env. The fastest setup is to symlink:

cd apps/jobs
ln -s ../api/.env .env

Or copy .env.example and fill in the values. The bare minimum to boot is REDIS_URL. Add DATABASE_URL + SMTP_* for emails to actually send, and OBJECT_STORAGE_* for media jobs to read source video files.

See .env.example for the full list with comments.

Start

From the repo root:

# In one terminal — API
pnpm api:dev

# In another terminal — worker (runs media + emails + maintenance in parallel)
pnpm jobs:dev

Or run a single worker directly:

cd apps/jobs
pnpm dev:emails        # just the emails queue
pnpm dev:media         # just the media queue
pnpm dev:maintenance   # just retention / dead-letter cleanup

The worker prints email-worker-ready, media-worker-ready, etc. once it connects to Redis. After that, every enqueue from the API logs an email-job-start → email-sent (or email-job-failed) line.

Smoke test

# Hit the API mail route — accepts immediately, returns 202 + jobIds
curl -X POST http://localhost:8787/mail \
  -H 'Content-Type: application/json' \
  -H 'x-api-key: <api key>' \
  -d '[{"to":"[email protected]","subject":"BullMQ test","content":"<p>hi</p>"}]'

# Watch the worker terminal for:
# [email-job-start] jobName=send bullmqJobId=… attempt=1
# [email-sent]      kind=raw [email protected] providerId=…

Manually re-trigger transcription (uploaded lesson videos)

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:

# 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" \
  -H "Authorization: Bearer <jwt>" \
  -H "cio-org-id: <organizationUuid>" \
  -i
HTTP Meaning
202 Job created and enqueued (success: true, data is the new media_job row).
503 OPENAI_KEY_MISSING — OPENAI_API_KEY is not set on the API.
400 ASSET_NOT_TRANSCRIBABLE — not an upload video with storage, or wrong asset kind.
409 Another active media_job already exists for this asset.

To inspect the queue without a UI:

redis-cli LLEN bull:emails:wait      # waiting jobs
redis-cli LLEN bull:emails:active    # currently processing
redis-cli LLEN bull:emails:failed    # exhausted retries

For a real UI, ClassroomIO mounts Workbench in apps/api at /admin/queues.

Behaviour matrix when the worker is down

Scenario API behaviour Job state
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

  • typically you want N API instances for HTTP concurrency and a smaller fixed number of worker instances tuned via *_WORKER_CONCURRENCY env vars.

Use node dist/index.js to run all queues in one process, or node dist/workers/<name>.js to run them as separate services (recommended for prod so a slow media job can't starve email throughput).

The included Dockerfile installs ffmpeg and ships the all-in-one entry point.