Push Notification Infrastructure Codemap
Architecture Overview
┌─────────────────────────────────────────────────────────────────────┐
│ Novu Framework (@novu/framework) │
│ Workflows registered via getWorkflowFactory() → workflow() │
│ Channels: step.push(), step.email(), step.inApp(), step.sms() │
│ Provider: OneSignal (via novu.subscribers.credentials.update) │
└─────────────────────────────────────────────────────────────────────┘
Packages involved: server/src/notification/, server/src/instantdb/, server/src/bullmq/, server/src/moderation/, server/src/templates/
1. Push Notification Type Definitions
File: server/src/notification/enums/push.enum.ts
42 push notification types across 6 categories:
| Category | Types |
|---|---|
| Account & Auth | WELCOME, EMAIL_VERIFICATION_REMINDER, PASSWORD_RESET_REQUEST, PASSWORD_CHANGED |
| Course & Consultation | COURSE_PURCHASE_CONFIRMATION, COURSE_AVAILABLE, UPCOMING_CONSULTATION_REMINDER, CONSULTATION_COMPLETED_FEEDBACK_REQUEST, CONSULTATION_CANCELLED, COURSE_COMPLETION_BADGE, NEW_COURSE_IN_CATEGORY |
| Withdrawals & Earnings | WITHDRAWAL_REQUEST_RECEIVED, WITHDRAWAL_APPROVED, WITHDRAWAL_DENIED, EARNINGS_UPDATE |
| Engagement | INACTIVE_USER_REENGAGEMENT, COURSE_DISCOUNT_ALERT, INSTRUCTOR_MESSAGE_NOTIFICATION |
| Admin & Support | NEW_COURSE_SUBMITTED, COURSE_APPROVED, COURSE_REJECTED, USER_REPORTED_ALERT |
| Financial & System | NEW_WITHDRAWAL_REQUEST, PAYOUT_PROCESSING_FAILURE, REVENUE_MILESTONE_ACHIEVEMENT, SUSPICIOUS_LOGIN_ATTEMPT, API_KEY_EXPIRY_WARNING, SERVICE_DOWNTIME_ALERT |
2. Core Workflow Registration & Factory
File: server/src/notification/workflows.service.ts
Factory Pattern (workflows.service.ts:120-131)
private getWorkflowFactory()
├── NODE_ENV=TEST → returns mock { id, trigger: async => ({}) }
└── Production → const { workflow } = require('@novu/framework')
→ returns `workflow(id, steps, { payloadSchema })`
Returns Novu's workflow() function. All workflows follow:
workflow(Workflows.XXX, async ({ step, payload }) => {
await step.push('step-name', async () => {
// data retrieval happens INSIDE step callbacks
// renders templates, constructs { subject, body, data }
});
}, { payloadSchema: z.object({...}) });
Workflow Registration (workflows.service.ts:1422-1449)
getWorkFlows() returns an array of 23 workflow instances:
| # | Workflow | Line | Push? |
|---|---|---|---|
| 1 | consultationReminderWorkflow() | 490 | ✅ (conditional) |
| 2 | consultationRescheduledWorkflow() | 592 | ❌ (inApp only) |
| 3 | consultationRescheduleApprovedWorkflow() | 629 | ❌ (inApp only) |
| 4 | consultationCancelledWorkflow() | 668 | ❌ (email + inApp) |
| 5 | consultationEnrollmentWorkflow() | 738 | ❌ (email + inApp) |
| 6 | bookingConfirmationWorkflow() | 818 | ❌ (email + inApp) |
| 7 | orderNotificationWorkflow() | 911 | ❌ (email only) |
| 8 | productEnrollmentWorkflow() | 967 | ❌ (email only) |
| 9 | productGiftReceivedWorkflow() | 999 | ❌ (email only) |
| 10 | productGiftSentWorkflow() | 1028 | ❌ (email only) |
| 11 | pushNotificationWorkflow() | 132 | ✅ Primary push |
| 12 | inAppNotificationWorkflow() | 165 | ❌ (inApp only) |
| 13 | emailWorkflow() | 201 | ❌ (email only) |
| 14 | productUpdateWorkflow() | 1059 | ❌ (email only) |
| 15 | sendPaymentNotificationWorkflow() | 1089 | ❌ (email only) |
| 16 | sendUserActionNotificationWorkflow() | 1120 | ❌ (email only) |
| 17 | otpWorkflow() | 232 | ❌ (sms + email) |
| 18 | withdrawalMethodVerifiedWorkflow() | 1258 | ❌ (email only) |
| 19 | withdrawalTransactionWorkflow() | 1180 | ❌ (email only) |
| 20 | sendCourseCompletionNotificationWorkflow() | 1150 | ❌ (email only) |
| 21 | multistepWorkflow() | 294 | ✅ Push + email + inApp + sms |
| 22 | adminSystemAlertWorkflow() | 459 | ❌ (inApp only) |
| 23 | chatNotificationWorkflow() | 1381 | ✅ Chat push with digest |
3. Push Notification Workflow (Single Channel)
File: server/src/notification/workflows.service.ts:132-163
Invocation Path
pushNotificationWorkflow().trigger({
to: subscriberId,
payload: {
userId: string,
template: PushNotification, // enum key
data: { TITLE?, MESSAGE? }
}
})
Step Flow
step.push('send-push-notification')
├── userService.getUser(payload.userId)
├── templateService.processTemplate(
│ payload.template, // PushNotification enum
│ NotificationChannel.PUSH, // 'push'
│ payload.data, // interpolation variables
│ user // user context for template
│ )
├── Validates: subject && body !== null
└── Returns: { subject, body, data }
Typical Caller Pattern
Triggered mostly via triggerWithCircuitBreaker() which wraps the trigger in:
CircuitBreakerStrategy: 60% error threshold, 8 req volume, 15s sleep, 90s windowRetryStrategy: 3 max retries, 15s max delay, exponential backoff (1.5s base)
4. Chat Message Push Notification Flow
Message Creation & Queuing
ModerationService (moderation.service.ts:270-303)
└── After moderation pass:
└── notificationService.addNotificationJob({
messageId, roomId, senderId, senderName,
messageType, content, timestamp, roomName,
roomType, entityType
})
└── notificationQueue.add('PROCESS_NOTIFICATION', data)
(queue: NOTIFICATION)
BullMQ Notification Processor
File: server/src/notification/processors/notification.processor.ts
NotificationProcessor.process(job)
└── job.name === 'PROCESS_NOTIFICATION'
└── job.data.messageId exists?
├── YES → processChatNotification(job)
└── NO → handle other types
processChatNotification(job):
├── 1. getMessageDetails(messageId)
│ → instantdbService.getMessage()
├── 2. Check message age threshold
│ → settingsService.getSettingValue('CHAT_NOTIFICATION_MAX_AGE_MINUTES')
│ → Default: 2 minutes
│ → If too old: markMessageNotificationProcessed() + return
├── 3. Route by room type:
│
│ PRIVATE:
│ ├── getNotificationRecipients(roomId, senderId, timestamp)
│ │ ├── getRoomById() + getRoomParticipants()
│ │ ├── Filter: skip sender
│ │ └── Filter: shouldNotifyUser() per recipient
│ │ ├── isUserInRoomView(userId, roomId) → ephemeral presence
│ │ └── getUserLastSeen(userId, roomId) → persistent read receipt
│ └── triggerChatNotificationWorkflow(recipientId, messageData)
│ → triggerTarget = { subscriberId: recipientId }
│
│ GROUP:
│ ├── notificationService.ensureRoomTopic(roomId, roomName)
│ │ → deriveRoomTopicKey(roomId) → `room-${roomId}`
│ │ → Check if topic exists via novu.topics.get()
│ │ → If not: novu.topics.create({ key, name: `Room: ${roomName}` })
│ └── triggerChatNotificationWorkflow(null, messageData)
│ → triggerTarget = { type: "Topic", topicKey: `room-${roomId}` }
│
└── 4. Mark message as notification processed
→ instantdbService.markMessageNotificationProcessed(messageId)
Chat Notification Workflow Definition
File: server/src/notification/workflows.service.ts:1381-1420
chatNotificationWorkflow():
├── step.digest('batch-chat-messages', () => {
│ amount: digestWindow || 5 seconds
│ digestKey: subscriberId || `room-${roomId}`
│ })
└── step.push('send-chat-notification', () => {
subject: payload.sender // sender name for private, room name for group
body: payload.content // formatted + truncated content
data: { messageId, roomId, messageType, timestamp }
})
Message Formatting
File: server/src/notification/services/message-formatter.service.ts
| Message Type | Notification Content |
|---|---|
| TEXT | Truncated to CHAT_NOTIFICATION_CONTENT_MAX_LENGTH (default: 100) + ellipsis |
| FILE / IMAGE / VIDEO / AUDIO / DOCUMENT | "You received a file" |
Digest windows:
- PRIVATE rooms:
CHAT_NOTIFICATION_DIGEST_WINDOW_DM(1s) - GROUP rooms:
CHAT_NOTIFICATION_DIGEST_WINDOW_GROUP(10s) - CATEGORY rooms:
CHAT_NOTIFICATION_DIGEST_WINDOW_CATEGORY(10s)
5. Event-Driven Push Notification Flow
File: server/src/notification/listeners/notification.listener.ts
Trigger Pattern
All event handlers follow:
@OnEvent(AppEvents.XXX)
async handleXxxEvent(payload) {
await this.workflowsService.triggerWithCircuitBreaker(() =>
this.workflowsService.someWorkflow().trigger({ to, payload })
);
}
Events → Workflow Mapping (with push)
| Event | Workflow | Push? |
|---|---|---|
SPACE_SESSION_LIVE | multistepWorkflow() | ✅ push: { data: { TITLE, MESSAGE } } |
SPACE_SESSION_REMINDER_24HR | multistepWorkflow() | ❌ (email + inApp) |
SPACE_SESSION_REMINDER_1HR | multistepWorkflow() | ❌ (email + inApp) |
SPACE_REGISTRATION_COMPLETED | multistepWorkflow() | ❌ (email + inApp) |
SPACE_REGISTRATION_CANCELLED | multistepWorkflow() | ❌ (email + inApp) |
COURSE_APPROVED | productUpdateWorkflow() | ❌ (email only) |
COURSE_REJECTION | productUpdateWorkflow() | ❌ (email only) |
CONSULTATION_APPROVED | productUpdateWorkflow() | ❌ (email only) |
CONSULTATION_REJECTED | productUpdateWorkflow() | ❌ (email only) |
PRODUCT_ENROLLMENT | productEnrollmentWorkflow() | ❌ (email only) |
The SPACE_SESSION_LIVE handler (notification.listener.ts:419-450) is the only event-driven path that triggers push notifications:
await this.workflowsService.multistepWorkflow().trigger({
to: payload.recipientUserId,
payload: {
userId: payload.recipientUserId,
push: { data: { TITLE: 'Space is live now', MESSAGE: 'The session has started. Join now.' } },
inapp: { data: { ... } },
data: { spaceId, sessionId },
priority: 'high',
},
})
6. Multistep Workflow (Multi-Channel)
File: server/src/notification/workflows.service.ts:294-457
Single trigger → up to 4 channel steps, each with conditional skip:
multistepWorkflow():
├── step.email('send-multistep-email')
│ ├── IF email.key → processTemplate() with Enum key
│ ├── IF email.data → use TITLE/MESSAGE directly
│ └── skip: !payload.email
│
├── step.push('send-multistep-push')
│ ├── IF push.key → processTemplate() with PushNotification enum
│ ├── IF push.data → use TITLE/MESSAGE directly
│ └── skip: !payload.push
│
├── step.inApp('send-multistep-inapp')
│ ├── IF inapp.key → processTemplate() with InAppNotification enum
│ ├── IF inapp.data → use TITLE/MESSAGE directly
│ └── skip: !payload.inapp
│
└── step.sms('send-multistep-sms')
├── IF sms.key → processTemplate() with SmsNotification enum
├── IF sms.data → use MESSAGE directly
└── skip: !payload.sms
Payload schema:
{
userId: string,
eventId?: AppEvents,
email?: { key?: EmailNotification, data?: { TITLE, MESSAGE } },
push?: { key?: PushNotification, data?: { TITLE, MESSAGE } },
inapp?: { key?: InAppNotification, data?: { TITLE, MESSAGE } },
sms?: { key?: SmsNotification, data?: { MESSAGE } },
data?: Record<string, any>,
priority?: 'low' | 'medium' | 'high' | 'urgent'
}
7. Consultation Reminder Push
Scheduling (BullMQ → ReminderProcessor)
File: server/src/bullmq/processors/reminder.processor.ts
ReminderProcessor.process(job)
├── job.name === 'CONSULTATION_REMINDER_24HR'
│ → reminderType = TWENTY_FOUR_HOURS
├── job.name === 'CONSULTATION_REMINDER_30MIN'
│ → reminderType = THIRTY_MINUTES
│
├── Validate session exists & status is SCHEDULED/UPCOMING
├── Fetch student + consultant
├── Trigger for student: workflow.trigger({ to: student.id, ... })
└── Trigger for consultant: workflow.trigger({ to: consultant.id, ... })
Reminder Workflow Definition
File: server/src/notification/workflows.service.ts:490-590
consultationReminderWorkflow():
├── Data retrieval helper: getConsultationReminderData()
│ ├── sessionsService.findOne(sessionId, [relations...])
│ ├── Load: consultation → consultant → user
│ └── Determine userForEmail + otherParty
│
├── step.email('send-consultation-reminder-email') ← ALWAYS
│ └── template: EmailNotification.UPCOMING_CONSULTATION_REMINDER
│ with MJML template CONSULTATION
│
└── Conditional: reminderType === THIRTY_MINUTES
├── step.push('send-30min-push-reminder')
│ → subject: 'Session Starting Soon'
│ → body: `Your consultation with ${otherParty.firstName} is in 30 minutes.`
│ → data: { sessionId, meetingLink }
└── step.inApp('send-30min-in-app-reminder')
→ subject: 'Session Starting Soon'
→ body: same as push
→ data: { sessionId, meetingLink }
8. Device Token Registration & Novu Sync
File: server/src/notification/notification.service.ts
Client → Server Flow
Client sends: POST /notification/device-token (or similar)
→ NotificationController (unimplemented in controller — called from other services)
saveDeviceToken(data: DeviceTokenDto, user?: User) → notification.service.ts:166
│
├── 1. Check existing: deviceTokenRepo.findOne({ deviceToken, fingerprint })
│
├── 2. Token already exists:
│ ├── Update lastActive
│ ├── If no user attached but user provided: attach user + sync credentials
│ └── Return 'success'
│
├── 3. New token:
│ ├── deviceTokenRepo.save({ deviceToken, fingerprint, user, lastActive })
│ └── If user provided: fetch all user tokens → sync to Novu
│ └── setCredential(user.id, allDeviceTokens)
│
└── setCredential(subscriberId, deviceTokens) → notification.service.ts:297
├── Resolve provider ID:
│ - Config: ONESIGNAL_PROVIDER_ID
│ - Normalize: 'onesignal' → 'one-signal'
│ - Fallback: NovuProviderId.OneSignal = 'one-signal'
└── novu.subscribers.credentials.update({
providerId,
credentials: { deviceTokens }
}, subscriberId)
Device Token Entity
File: server/src/notification/entities/device-tokens.entity.ts
| Column | Type | Notes |
|---|---|---|
id | UUID (PK) | Auto-generated |
deviceToken | string | Push device token |
fingerprint | string | Device fingerprint |
user | Relation → User | Nullable (attached on first login) |
lastActive | timestamp | Updated on token reuse |
createdAt / updatedAt | timestamps | Auto |
9. Topic-Based Group Push
Topic Lifecycle
Room Creation (instantdb.service.ts:365-419):
getOrCreateRoom() or createRoom()
└── IF type === GROUP:
└── notificationService.ensureRoomTopic(roomId, roomName)
Topic Initialization (notification.service.ts:338-359):
ensureRoomTopic(roomId, roomName):
├── deriveRoomTopicKey(roomId) → `room-${roomId}`
├── Try: novu.topics.get(topicKey)
│ ├── Found: log + return
│ └── Not found: novu.topics.create({ key, name: `Room: ${roomName}` })
└── Return topicKey
Participant Subscription (instantdb.service.ts:137-188):
createRoomParticipant({ entityType, entityId, userId, role })
├── Create participant in InstantDB
└── IF room.type === GROUP:
└── notificationService.addSubscriberToTopic([userId], topicKey)
Participant Unsubscription (instantdb.service.ts:310-332):
leaveRoom(roomId, userId)
├── Delete participant from InstantDB
└── IF room.type === GROUP:
└── notificationService.removeSubscriberFromTopic(userId, topicKey)
Topic Notification Delivery
From Chat Processor (notification.processor.ts:287-296):
// GROUP room → topic trigger
triggerTarget = {
type: "Topic",
topicKey: `room-${messageData.roomId}`
}
await workflowsService.chatNotificationWorkflow().trigger({
to: triggerTarget,
payload: { ... }
})
Generic Topic Notification (notification.service.ts:271-286):
notifyTopic(topicKey: Topics | string, payload, workflowId?):
├── resolveTopicWorkflowId(topicKey)
│ ├── Topics.ADMIN → Workflows.ADMIN_SYSTEM_ALERT
│ └── default → undefined (warns + returns null)
└── novu.trigger({ workflowId, to: { type: "Topic", topicKey }, payload })
10. Resilience & Error Handling
File: server/src/notification/workflows.service.ts:1293-1379
Circuit Breaker + Retry
@UseResilience(
CircuitBreakerStrategy({
errorThresholdPercentage: 60,
requestVolumeThreshold: 8,
sleepWindowInMilliseconds: 15000,
rollingWindowInMilliseconds: 90000,
timeoutInMilliseconds: 5000,
}),
RetryStrategy({
maxRetries: 3,
maxDelay: 15000,
backoff: new ExponentialBackoff({ baseDelay: 1500 }),
})
)
async triggerWithCircuitBreaker<T>(workflowTrigger: () => Promise<T>)
isRetryableError() classifies errors: timeouts, network errors, template processing errors, user lookup errors, Novu validation errors, rate limiting (429), service unavailable (502/503/504).
Chat Notification Failure Isolation
In NotificationProcessor (notification.processor.ts:136-142):
- Errors are logged but NOT thrown (prevents job retry)
- Chat message creation is decoupled from notification delivery
- "Chat notification failed, but chat message creation succeeded"
11. Test Trigger Endpoint
File: server/src/notification/notification.controller.ts:99-138
POST /notification/test-trigger with { channel: 'push' | 'email' | 'inapp' | 'sms' | 'multistep' }:
| Channel | Workflow | Trigger Payload |
|---|---|---|
push | pushNotificationWorkflow() | { template: PushNotification.WELCOME, data: { TITLE, MESSAGE } } |
email | emailWorkflow() | { template: EmailNotification.WELCOME, data: { TITLE, MESSAGE } } |
inapp | inAppNotificationWorkflow() | { template: InAppNotification.CUSTOM, data: { TITLE, MESSAGE } } |
sms | otpWorkflow() | { template: SmsNotification.OTP_CODE, channel: SMS, data: { CODE, TITLE, MESSAGE } } |
multistep | multistepWorkflow() | { email, push, inapp, sms all with data } |
12. Novu Subscriber Management
File: server/src/notification/notification.service.ts
| Method | Line | Description |
|---|---|---|
createSubscriber(user) | 156 | Creates Novu subscriber with id, email, name |
updateSubscriber(user) | 219 | Patches subscriber email + name |
deactivateSubscriber(subscriberId) | 230 | Deletes subscriber from Novu |
getSubscribers() / listSubscribers() | 195/203 | Lists subscribers (limit 20) |
generateSubscriberHash(subscriberId) | 311 | HMAC-SHA256 hash for secure inbox |
resubscribeUserToGroupRooms(userId) | 373 | Re-subscribes user to all GROUP room topics |
resubscribeUserToRoomTopic(userId, roomId) | 409 | Re-subscribes user to single room topic |
Topic Management
| Method | Line | Description |
|---|---|---|
addSubscriberToTopic(subscriberIds, topicKey) | 208 | Subscribe users to a topic |
removeSubscriberFromTopic(subscriberId, topicKey) | 234 | Unsubscribe user from topic |
createTopic(key, name) | 263 | Create a Novu topic |
getTopic(key) | 258 | Get topic details |
syncListToTopic(roles, topic) | 248 | Sync user roles to a topic |
ensureRoomTopic(roomId, roomName) | 338 | Get-or-create room topic |
deriveRoomTopicKey(roomId) | 325 | room-${roomId} |
getNotificationTriggerTarget(roomType, recipientId, roomId) | 435 | Returns { subscriberId } or { type: "Topic", topicKey } |
13. Key Enums & Constants
File: server/src/notification/enums/
| Enum | File | Purpose |
|---|---|---|
PushNotification | push.enum.ts | 42 push template keys |
Workflows | workflows.enum.ts | 40+ workflow ID strings |
NotificationChannel | notification-channel.enum.ts | EMAIL, PUSH, IN_APP, SMS |
Topics | topics.enum.ts | ADMIN, USERS, SYSTEM |
NovuProviderId | novu.enums.ts | OneSignal = 'one-signal' |
NovuRecipientType | novu.enums.ts | Topic |
AppEvents | common/enums/app-events.enum.ts | 170+ domain events |
14. Data Flow Diagrams
Push Notification (Single Channel)
[Service/Controller]
→ workflowsService.pushNotificationWorkflow().trigger({
to: subscriberId,
payload: { userId, template, data }
})
→ Novu Framework → step.push('send-push-notification')
→ userService.getUser(userId)
→ templateService.processTemplate(template, 'push', data, user)
→ Returns { subject, body, data }
→ Novu dispatches to OneSignal
→ Device receives push
Chat Message Push
[User sends message]
→ ModerationService processes message
→ notificationService.addNotificationJob({ messageId, ... })
→ BullMQ Queue (NOTIFICATION)
→ NotificationProcessor.processChatNotification()
→ getMessageDetails()
→ Check message age (CHAT_NOTIFICATION_MAX_AGE_MINUTES)
→ Route: PRIVATE → per-recipient with shouldNotifyUser() filtering
GROUP → topic-based single trigger
→ MessageFormatterService.formatNotificationContent()
→ workflowsService.chatNotificationWorkflow().trigger()
→ step.digest() for batching
→ step.push('send-chat-notification') → { subject, body, data }
→ markMessageNotificationProcessed()
Device Token Registration
[Client registers device]
→ notificationService.saveDeviceToken({ token, fingerprint }, user)
→ Upsert in device-tokens table
→ Fetch all tokens for user
→ setCredential(userId, tokens)
→ novu.subscribers.credentials.update({
providerId: 'one-signal',
credentials: { deviceTokens: [...] }
})
Event-Driven Push (Space Session Live)
[Space goes live]
→ EventEmitter emits AppEvents.SPACE_SESSION_LIVE
→ NotificationListener.handleSpaceSessionLiveEvent()
→ triggerWithCircuitBreaker()
→ CircuitBreakerStrategy + RetryStrategy
→ multistepWorkflow().trigger({
to: recipientUserId,
payload: { push: { data: { TITLE, MESSAGE } }, inapp: {...}, ... }
})
→ Novu Framework
→ step.push('send-multistep-push') → subject, body, data
→ step.inApp('send-multistep-inapp') → subject, body, data
15. File Index
| File | Path | Key Lines |
|---|---|---|
| Workflows Service | server/src/notification/workflows.service.ts | 132, 294, 490, 1059, 1309, 1381, 1422 |
| Notification Service | server/src/notification/notification.service.ts | 166, 208, 271, 297, 338, 373, 435 |
| Notification Processor | server/src/notification/processors/notification.processor.ts | 70, 149, 183, 270, 350 |
| Notification Listener | server/src/notification/listeners/notification.listener.ts | 59, 201, 329, 374, 419, 452 |
| Notification Controller | server/src/notification/notification.controller.ts | 99 |
| Message Formatter | server/src/notification/services/message-formatter.service.ts | 22, 92 |
| Reminder Processor | server/src/bullmq/processors/reminder.processor.ts | 32 |
| Push Enum | server/src/notification/enums/push.enum.ts | 1-43 |
| Workflows Enum | server/src/notification/enums/workflows.enum.ts | 1-102 |
| Notification Channel Enum | server/src/notification/enums/notification-channel.enum.ts | 1-6 |
| Topics Enum | server/src/notification/enums/topics.enum.ts | 1-5 |
| Novu Enums | server/src/notification/enums/novu.enums.ts | 1-7 |
| Device Token DTO | server/src/notification/dto/device-token.dto.ts | 1-11 |
| Device Token Entity | server/src/notification/entities/device-tokens.entity.ts | 1-26 |
| InstantDB Service | server/src/instantdb/instantdb.service.ts | 137, 247, 285, 365, 758, 905, 927 |
| Instant Listener | server/src/instantdb/listeners/instant.listener.ts | 51, 98, 219 |
| Moderation Service | server/src/moderation/moderation.service.ts | 287 |
| App Events | server/src/common/enums/app-events.enum.ts | 1-171 |
| Template Service | server/src/templates/template.service.ts | (template processing) |