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
npx prisma db push --accept-data-loss— apply schema changesnpm run seed:notifications— seed system-default templates- Set
OPTICRM_API_KEYandOPTICRM_BASE_DOMAINon the web service - Deploy
npm run workeras a second Coolify service - Smoke test: enroll yourself in a course, watch for the
ENROLL_WELCOMEemail
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.
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
LMSNotificationrow withstatus: 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
| Variable | Notes |
|---|---|
DATABASE_URL | Postgres connection string |
REDIS_URL | Redis — same instance as OptiCRM is fine |
NEXTAUTH_SECRET | Must match OptiCRM exactly — drives shared SSO |
AUTH_SECRET | Same value as NEXTAUTH_SECRET (NextAuth v5 alias) |
NEXTAUTH_URL | https://learn.opticrm.app |
OPTICRM_API_KEY | Shared bearer token — must match what OptiCRM accepts |
OPTICRM_BASE_DOMAIN | opticrm.app in prod, override for staging |
Recommended
| Variable | Notes |
|---|---|
RESEND_API_KEY | Email provider |
EMAIL_FROM | OptiLearn <learn@opticrm.app> |
R2_* | Cloudflare R2 — see Architecture — File Uploads |
FCM_SERVICE_ACCOUNT_JSON | Firebase service account JSON for push |
CRON_SECRET | Bearer token for hourly cron endpoints |
JITSI_RECORDING_API_TOKEN | Only if Jibri recording is enabled |
JITSI_RECORDING_WEBHOOK_SECRET | Same — paired with the API token |
NOTIFICATION_UNSUB_SECRET | HMAC secret for unsubscribe URLs (falls back to NEXTAUTH_SECRET) |
APP_URL | Used by template helpers (default https://learn.opticrm.app) |
OPTICRM_PROTOCOL | https (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:
| Field | Value |
|---|---|
| Repo | Same as web |
| Build command | npm install && npx prisma generate |
| Start command | npm run worker |
| Replicas | 1 |
| Restart policy | Always |
| Environment | Copy 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
- Enroll a test student in any course
- 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
- 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)
LMSNotificationrows created for the test? (statustells you why if it's not SENT)
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
All notifications FAILED with missing_template | Seed skipped | Run npm run seed:notifications |
All notifications PENDING forever | Worker not running | Deploy the worker service |
| Notifications work locally, fail in prod | Worker env missing RESEND_API_KEY / OPTICRM_API_KEY | Copy env from web to worker |
| Some users get notifications, others don't | Daily-cap suppression triggering | Expected — see Suppression rules |
| Recipient name shows as "Student" | OptiCRM Contact lookup failed | Verify OPTICRM_API_KEY + slug routing |
401s from /api/lms/* | OPTICRM_API_KEY mismatch | Sync the secret between OptiCRM and OptiLearn |
Related
- Notifications System — engine internals
- Worker Deployment — operating the worker
- OptiCRM Integration — env vars + slug routing
- Notifications API — preferences endpoints