Notification Pipeline
Event-driven notification system supporting in-app, email, and web push delivery channels with per-user, per-guild preference control.
Architecture Overview
flowchart TD
A["Domain Event (e.g. event.published)"] --> B[NotificationEventHandler]
B -->|determines recipients + channels| C[CreateNotificationUseCase]
C -->|checks preferences, filters disabled channels| D{Channels enabled?}
D -->|No| E[Skip — no notification created]
D -->|Yes| F["Create Notification + Delivery rows"]
F -->|in_app → sent immediately| G[NotificationCreatedEvent]
F -->|email/web_push → pending| G
G -->|"@OnEvent"| H[NotificationDeliveryProcessor]
H --> I[MailService.sendNotificationEmail]
H --> J[WebPushService.sendToUser]
Data Model
Notification
| Field | Description |
|---|---|
id | UUID |
userId | Recipient |
organizationId | Guild context (nullable for platform-wide) |
type | NotificationType enum |
title | Notification headline |
body | Notification body text |
payload | JSON — arbitrary context (eventId, etc.) |
readAt | When user read it (nullable) |
dismissedAt | When user dismissed it (nullable) |
Indexes: (userId, readAt), (userId, dismissedAt), (userId, organizationId).
NotificationDelivery
Audit log per channel per notification.
| Field | Description |
|---|---|
notificationId | Parent notification |
channel | in_app, email, or web_push |
status | pending, sent, or failed |
sentAt | Delivery timestamp |
failedAt | Failure timestamp |
errorMessage | Error detail on failure |
NotificationPreference
Per-user opt-in/out for each notification type and channel.
| Field | Description |
|---|---|
userId | User |
organizationId | Guild (nullable — null = global default) |
type | NotificationType |
channel | NotificationChannel |
enabled | Boolean (default true) |
Unique constraint: (userId, organizationId, type, channel).
PushSubscription
Browser push subscription endpoints per user (VAPID/Web Push).
| Field | Description |
|---|---|
userId | User |
endpoint | Push service URL (unique) |
p256dh | Client public key |
auth | Client auth secret |
Notification Types
| Type | Trigger | Default Channels |
|---|---|---|
event_published | New event published to guild | in_app |
event_updated | Published event details changed | in_app, email |
event_starting_soon | Reminder before event start (cron) | in_app, email |
event_cancelled | Event cancelled | in_app |
signup_confirmed | Signup status changed by officer | in_app |
application_submitted | New guild application received | in_app, email |
application_reviewed | Application accepted/declined | in_app, email |
membership_role_changed | Guild role changed | in_app |
membership_removed | Membership ended | in_app |
battlenet_token_expired | Battle.net token needs re-authorization | in_app |
Event Handlers
Domain events are emitted by their respective services and caught by NotificationEventHandler:
| Domain Event | Recipients |
|---|---|
event.published | All active/trial guild members (excluding creator) |
event.cancelled | Participants with status confirmed/waitlist |
event.updated | Participants with status confirmed/waitlist |
signup.created | Signup user (when actor ≠ user) |
membership.role.changed | Member whose role changed |
application.submitted | Officers with canManageMembers |
application.reviewed | Applicant |
membership.removed | Removed member |
The event_starting_soon type is handled differently — EventSchedulerService.sendEventReminders() runs on a 5-minute cron and creates notifications for participants of events starting within EVENT_REMINDER_LEAD_MINUTES (default 15).
Preference Resolution
When CreateNotificationUseCase runs:
- Load
NotificationPreferencerows for the user, type, and requested channels. - Check org-specific preferences first, then global (where
organizationIdis null). - Remove channels where
enabled = false. - If no channels remain, skip — no notification is created.
This means users can disable email notifications globally but re-enable them for a specific guild.
Delivery Channels
In-App
Marked sent immediately on creation. Retrieved via REST polling (GET /notifications).
Email
Queued as pending delivery. NotificationDeliveryProcessor calls MailService.sendNotificationEmail(). On success → sent; on failure → failed with error message.
Web Push
Uses VAPID protocol via the web-push library. WebPushService.sendToUser() sends to all registered PushSubscription endpoints for the user. Invalid subscriptions (404/410 responses) are automatically removed.
Configuration:
VAPID_PUBLIC_KEY— shared with frontend for subscriptionVAPID_PRIVATE_KEY— signing keyVAPID_SUBJECT— contact URL/email
API Endpoints
Notifications
| Method | Path | Description |
|---|---|---|
| GET | /notifications | Paginated list (query: organizationId, unreadOnly, page, pageSize) |
| GET | /notifications/unread-count | Unread count (query: organizationId) |
| PATCH | /notifications/:id/read | Mark one read |
| PATCH | /notifications/read-all | Mark all read |
| PATCH | /notifications/:id/dismiss | Dismiss one |
| PATCH | /notifications/dismiss-all | Dismiss all |
Preferences
| Method | Path | Description |
|---|---|---|
| GET | /notifications/preferences | Get user preferences |
| PUT | /notifications/preferences | Update preferences |
Web Push Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /notifications/push/vapid-key | Get VAPID public key |
| POST | /notifications/push/subscribe | Register subscription |
| DELETE | /notifications/push/subscribe | Remove subscription |
Frontend Integration
RTK Query Hooks
useGetNotificationsQuery— paginated notification listuseGetUnreadCountQuery— badge/counteruseMarkNotificationReadMutation/useMarkAllReadMutationuseDismissNotificationMutation/useDismissAllNotificationsMutationuseGetNotificationPreferencesQuery/useUpdateNotificationPreferencesMutationuseGetVapidKeyQuery/useSubscribePushMutation/useUnsubscribePushMutation
Polling
There is no WebSocket for notification delivery. The client relies on REST polling and refetchOnMountOrArgChange to keep the unread count current.
Cron Jobs
| Job | Schedule | Description |
|---|---|---|
| Event reminders | Every 5 min | event_starting_soon notifications |