Skip to main content

notifications app

Cross-cutting notification fan-out. Most apps emit notifications; this app handles delivery.

Models (4)

  • Notification — single in-app notification; read flag, action URL, event_type
  • NotificationPreference — per-user, per-event-type channel toggles (in_app / email / push)
  • ForumSubscription — subscriptions to forum threads (for new-reply notifications)
  • DigestPreference — per-user digest config (daily / weekly), with which event categories included

Key endpoints

URLPurpose
GET /api/notifications/List my notifications (paginated, sorted by date desc)
PATCH /api/notifications/<id>/read/Mark as read
POST /api/notifications/mark-all-read/Bulk mark read
GET /api/notifications/unread-count/For badge in navbar
GET /api/notification-preferences/My preferences
PATCH /api/notification-preferences/Update preferences
GET /api/digest-preferences/My digest config

Permissions

  • IsAuthenticated — all endpoints (own notifications only)

Background tasks

  • send_notification_emails(notification_ids) — batches per recipient, sends via email backend
  • generate_digest_emails — cron at 06:00 UTC daily; assembles per-user digest from previous day's notifications
  • cleanup_old_notifications — daily; removes read notifications older than 90 days

External integrations

  • Email backend (apps.platform.PlatformEmailConfig)
  • Mobile push (FCM / APNs via Capacitor's push registration — token stored in User.push_tokens)

Notable patterns

Notification fan-out pattern

Other apps create notifications by importing the helper:

from apps.notifications.services import notify

notify(
recipient=user,
event_type="forum.new_reply",
title="New reply in 'Spring Formal Planning'",
body="Alice replied to your post.",
action_url=f"/chapter/engage/forums/{forum.id}/posts/{post.id}",
metadata={"post_id": post.id},
)

The helper:

  1. Creates the Notification row (in-app)
  2. Looks up the user's NotificationPreference for this event_type
  3. Queues an email task if enabled
  4. Sends a push if enabled and the user has a registered token
  5. Adds to the digest queue if the user has digests enabled

Event types

Defined in apps/notifications/event_types.py (constants):

EVENT_TYPES = [
"bulletin.new_chapter",
"bulletin.new_org",
"forum.new_thread",
"forum.new_reply",
"forum.mention",
"election.opened",
"election.reminder",
"election.results",
"billing.invoice_issued",
"billing.payment_due",
"billing.payment_confirmed",
"approval.requested",
"approval.decided",
"event.invited",
"event.reminder",
"event.changed",
"learning.assigned",
"learning.due_soon",
"learning.certificate_ready",
"compliance.requirement_due",
"compliance.overdue",
"recognition.received",
"messages.new_dm",
"messages.group_mention",
]

Each has a default channel set (e.g., payment_due defaults to email + push; recognition.received defaults to in-app only).

Digest

If a user has digests enabled for a category, individual notifications still appear in-app but emails are suppressed. The daily / weekly digest collects them into one summary email.

Digest email template at templates/notifications/digest_email.html.

Read state

Notification.read boolean; auto-set to True when the user clicks through. Unread count drives the navbar badge; computed via:

Notification.objects.filter(recipient=user, read=False).count()

Cached for 30s to keep badge updates fast.

Signals

  • post_save on Notification — triggers email send (if preferences allow)

Code paths

  • Models: backend/apps/notifications/models.py
  • Service: backend/apps/notifications/services.py
  • Tasks: backend/apps/notifications/tasks.py
  • Email templates: backend/apps/notifications/templates/