Skip to main content

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

FieldDescription
idUUID
userIdRecipient
organizationIdGuild context (nullable for platform-wide)
typeNotificationType enum
titleNotification headline
bodyNotification body text
payloadJSON — arbitrary context (eventId, etc.)
readAtWhen user read it (nullable)
dismissedAtWhen user dismissed it (nullable)

Indexes: (userId, readAt), (userId, dismissedAt), (userId, organizationId).

NotificationDelivery

Audit log per channel per notification.

FieldDescription
notificationIdParent notification
channelin_app, email, or web_push
statuspending, sent, or failed
sentAtDelivery timestamp
failedAtFailure timestamp
errorMessageError detail on failure

NotificationPreference

Per-user opt-in/out for each notification type and channel.

FieldDescription
userIdUser
organizationIdGuild (nullable — null = global default)
typeNotificationType
channelNotificationChannel
enabledBoolean (default true)

Unique constraint: (userId, organizationId, type, channel).

PushSubscription

Browser push subscription endpoints per user (VAPID/Web Push).

FieldDescription
userIdUser
endpointPush service URL (unique)
p256dhClient public key
authClient auth secret

Notification Types

TypeTriggerDefault Channels
event_publishedNew event published to guildin_app
event_updatedPublished event details changedin_app, email
event_starting_soonReminder before event start (cron)in_app, email
event_cancelledEvent cancelledin_app
signup_confirmedSignup status changed by officerin_app
application_submittedNew guild application receivedin_app, email
application_reviewedApplication accepted/declinedin_app, email
membership_role_changedGuild role changedin_app
membership_removedMembership endedin_app
battlenet_token_expiredBattle.net token needs re-authorizationin_app

Event Handlers

Domain events are emitted by their respective services and caught by NotificationEventHandler:

Domain EventRecipients
event.publishedAll active/trial guild members (excluding creator)
event.cancelledParticipants with status confirmed/waitlist
event.updatedParticipants with status confirmed/waitlist
signup.createdSignup user (when actor ≠ user)
membership.role.changedMember whose role changed
application.submittedOfficers with canManageMembers
application.reviewedApplicant
membership.removedRemoved 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:

  1. Load NotificationPreference rows for the user, type, and requested channels.
  2. Check org-specific preferences first, then global (where organizationId is null).
  3. Remove channels where enabled = false.
  4. 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 subscription
  • VAPID_PRIVATE_KEY — signing key
  • VAPID_SUBJECT — contact URL/email

API Endpoints

Notifications

MethodPathDescription
GET/notificationsPaginated list (query: organizationId, unreadOnly, page, pageSize)
GET/notifications/unread-countUnread count (query: organizationId)
PATCH/notifications/:id/readMark one read
PATCH/notifications/read-allMark all read
PATCH/notifications/:id/dismissDismiss one
PATCH/notifications/dismiss-allDismiss all

Preferences

MethodPathDescription
GET/notifications/preferencesGet user preferences
PUT/notifications/preferencesUpdate preferences

Web Push Endpoints

MethodPathDescription
GET/notifications/push/vapid-keyGet VAPID public key
POST/notifications/push/subscribeRegister subscription
DELETE/notifications/push/subscribeRemove subscription

Frontend Integration

RTK Query Hooks

  • useGetNotificationsQuery — paginated notification list
  • useGetUnreadCountQuery — badge/counter
  • useMarkNotificationReadMutation / useMarkAllReadMutation
  • useDismissNotificationMutation / useDismissAllNotificationsMutation
  • useGetNotificationPreferencesQuery / useUpdateNotificationPreferencesMutation
  • useGetVapidKeyQuery / 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

JobScheduleDescription
Event remindersEvery 5 minevent_starting_soon notifications