DocsAdministration

Notifications Setup

Post-deploy checklist for the notifications + worker system

OptiLearn's notification engine has four moving parts that need to be configured after every deploy. Skip any of them and notifications silently fail (or worse, succeed for some users and fail for others).

Quick checklist

  1. npx prisma db push --accept-data-loss — apply schema changes
  2. npm run seed:notifications — seed system-default templates
  3. Set OPTICRM_API_KEY and OPTICRM_BASE_DOMAIN on the web service
  4. Deploy npm run worker as a second Coolify service
  5. Smoke test: enroll yourself in a course, watch for the ENROLL_WELCOME email

Each step in detail below.

Step 1 — Apply schema changes

After every deploy that touches prisma/schema.prisma:

npx prisma db push --accept-data-loss

In the prod Coolify shell, with DATABASE_URL set. The --accept-data-loss flag is needed when columns are renamed or dropped — otherwise db push refuses.

Warning

Always back up the database immediately before running this in prod. --accept-data-loss will drop columns without a confirmation prompt.

Step 2 — Seed notification templates

npm run seed:notifications

This script reads NOTIFICATION_DEFINITIONS (31 keys) and upserts one NotificationTemplate row per key with institutionId = null (system default). Idempotent — re-running won't duplicate.

If you skip this:

  • Every notification send writes a LMSNotification row with status: FAILED, failureReason: missing_template
  • Users get nothing and there's no UI signal
  • The admin notifications-analytics page (Phase 10) will show 100% failures

Run after every deploy that adds new notification keys. Editing copy in the seed file is also how you change system-default templates — re-run the seed after editing.

Step 3 — Environment variables

Set these on the OptiLearn web service (and copy to the worker service):

Required

VariableNotes
DATABASE_URLPostgres connection string
REDIS_URLRedis — same instance as OptiCRM is fine
NEXTAUTH_SECRETMust match OptiCRM exactly — drives shared SSO
AUTH_SECRETSame value as NEXTAUTH_SECRET (NextAuth v5 alias)
NEXTAUTH_URLhttps://learn.opticrm.app
OPTICRM_API_KEYShared bearer token — must match what OptiCRM accepts
OPTICRM_BASE_DOMAINopticrm.app in prod, override for staging
VariableNotes
RESEND_API_KEYEmail provider
EMAIL_FROMOptiLearn <learn@opticrm.app>
R2_*Cloudflare R2 — see Architecture — File Uploads
FCM_SERVICE_ACCOUNT_JSONFirebase service account JSON for push
CRON_SECRETBearer token for hourly cron endpoints
JITSI_RECORDING_API_TOKENOnly if Jibri recording is enabled
JITSI_RECORDING_WEBHOOK_SECRETSame — paired with the API token
NOTIFICATION_UNSUB_SECRETHMAC secret for unsubscribe URLs (falls back to NEXTAUTH_SECRET)
APP_URLUsed by template helpers (default https://learn.opticrm.app)
OPTICRM_PROTOCOLhttps (default), http for local docker

Step 4 — Deploy the worker

The notification queues, digest scanner, re-engagement scanner, and due-reminder scanner all run inside a separate worker process. The Next.js server only enqueues — without the worker, every notification stays PENDING forever.

Coolify config

Add a second service in the same Coolify project as the web app:

FieldValue
RepoSame as web
Build commandnpm install && npx prisma generate
Start commandnpm run worker
Replicas1
Restart policyAlways
EnvironmentCopy the entire env from the web service

Full details: Worker Deployment.

Verify it's running

Check Coolify logs for:

[worker] starting notification workers...
[worker] ready

Step 5 — Smoke test

  1. Enroll a test student in any course
  2. Within ~30 seconds:
    • In-app: bell badge in the header increments
    • Email: arrives at the student's address (Resend dashboard logs delivery)
    • Push: arrives if the student has a device registered
  3. Open /settings/notifications — every category card should render with the right toggle states

If anything is missing, walk through:

  • Worker running? (Coolify status)
  • Templates seeded? (npm run seed:notifications)
  • Resend API key set on the worker? (env tab)
  • LMSNotification rows created for the test? (status tells you why if it's not SENT)

Common pitfalls

SymptomCauseFix
All notifications FAILED with missing_templateSeed skippedRun npm run seed:notifications
All notifications PENDING foreverWorker not runningDeploy the worker service
Notifications work locally, fail in prodWorker env missing RESEND_API_KEY / OPTICRM_API_KEYCopy env from web to worker
Some users get notifications, others don'tDaily-cap suppression triggeringExpected — see Suppression rules
Recipient name shows as "Student"OptiCRM Contact lookup failedVerify OPTICRM_API_KEY + slug routing
401s from /api/lms/*OPTICRM_API_KEY mismatchSync the secret between OptiCRM and OptiLearn