services/api/src/modules/notifications/notification.events.js
Overview
This module is the domain-facing entry point for notification events. It exposes a normalized event vocabulary and a single async function that stores a notification event in the outbox, then attempts delivery immediately. The file keeps domain code away from the email provider, while preserving the persisted domain transition even when notifications fail fileciteturn0file0turn0file2.
It sits between the domain layer and the notification persistence and delivery modules. The outbox repository handles storage, and the notification service handles recipient resolution and email sending fileciteturn0file0turn0file2.
[!IMPORTANT] Notification failures are absorbed here. A failed delivery does not roll back the domain change that already persisted fileciteturn0file0turn0file2.
How it works
flowchart TD
Caller[Domain caller] --> Emit[emitNotificationEvent]
Emit --> Repo[createOutboxEntry]
Repo --> Outbox[NotificationOutbox]
Emit --> Deliver[deliverOutboxEntry]
Deliver --> Service[notification service]
Repo --> Log1[console error outbox persistence failed]
Deliver --> Log2[console error email delivery failed]
Deliver --> Result[Outbox entry returned]
The module first builds EVENT_TYPES from NOTIFICATION_EVENT_TYPES. That keeps the domain event names aligned with the schema-defined enum in NotificationOutbox.js fileciteturn0file0turn0file9.
When emitNotificationEvent runs, it creates an outbox row through createOutboxEntry. If that step fails, it logs OUTBOX_PERSISTENCE_FAILED and returns null. If persistence succeeds, it calls deliverOutboxEntry. Delivery errors are logged as EMAIL_DELIVERY_FAILED, but the outbox entry still returns to the caller fileciteturn0file0turn0file2.
[!NOTE] The function does not call the email provider directly. That responsibility stays inside
notification.service.jsandnotification.client.jsfileciteturn0file0turn0file2turn0file6.
Key components
{
"title": "EVENT_TYPES",
"content": "Frozen map of notification event names derived from NOTIFICATION_EVENT_TYPES. It keeps the domain event vocabulary aligned with the schema.",
"type": "enum",
"filePath": "services/api/src/modules/notifications/notification.events.js",
"badges": [
"public",
"read-only"
]
}
{
"title": "emitNotificationEvent",
"content": "Creates an outbox entry for a domain event and then tries to deliver it synchronously. It returns the outbox entry, or null when persistence fails.",
"type": "function",
"filePath": "services/api/src/modules/notifications/notification.events.js",
"badges": [
"public"
]
}
Data & fields
[
{
"name": "eventType",
"type": "string",
"required": true,
"description": "Notification event name. It is written to the outbox entry."
},
{
"name": "recipientUserId",
"type": "string | null",
"description": "Target user identifier. It defaults to null."
},
{
"name": "recipientChannel",
"type": "string",
"required": true,
"description": "Delivery channel stored in the outbox entry."
},
{
"name": "payload",
"type": "object",
"description": "Event payload. It defaults to an empty object."
}
]
EVENT_TYPES[badge:public] [badge:read-only]object— Frozen key-value map where each event name maps to itself.eventType[badge:required]string— The domain event to store and deliver.recipientUserId[badge:optional]string | null— The user receiving the notification.recipientChannel[badge:required]string— The channel used by the delivery flow.payload[badge:optional]object— Data passed to the notification template builder.
Behaviors
EVENT_TYPES— NormalizesNOTIFICATION_EVENT_TYPESinto a frozen lookup object.emitNotificationEvent— Persists a notification outbox entry, tries to deliver it, and returns the entry ornull.emitNotificationEvent— LogsOUTBOX_PERSISTENCE_FAILEDwhen outbox creation throws.emitNotificationEvent— LogsEMAIL_DELIVERY_FAILEDwhen delivery throws, without failing the caller.
Workflows
sequenceDiagram
participant Caller as Domain caller
participant Events as notification.events
participant Repo as notification.repository
participant Service as notification.service
Caller->>Events: emitNotificationEvent
Events->>Repo: createOutboxEntry
alt persistence fails
Repo-->>Events: throw
Events-->>Caller: null
else persistence ok
Repo-->>Events: outboxEntry
Events->>Service: deliverOutboxEntry
alt delivery fails
Service-->>Events: throw
Events-->>Caller: outboxEntry
else delivery ok
Service-->>Events: result
Events-->>Caller: outboxEntry
end
end
- Call
emitNotificationEvent| The caller passes the event name and optional recipient data. - Persist the outbox entry |
createOutboxEntrywrites the event toNotificationOutbox. - Attempt delivery |
deliverOutboxEntrytries to send the notification immediately. - Return the result | Persistence failure returns
null; delivery failure still returns the stored entry.
Gotchas & edge cases
[!WARNING]
emitNotificationEventonly returnsnullwhen outbox persistence fails. Delivery errors are absorbed and logged instead fileciteturn0file0turn0file2.
[!WARNING] This module does not validate delivery support itself.
notification.service.jslater rejects unsupported recipient channels such as anything other thanprimary_emailwith aUNSUPPORTED_RECIPIENT_CHANNELerror .
[!NOTE] The module is used after domain writes. The auth lifecycle calls
emitNotificationEventafter persisting state changes, and the digest job also imports it for scheduled mail flows fileciteturn0file14turn0file3.
Related
services/api/src/modules/notifications/notification.repository.js stores the outbox entry and updates its status. It owns createOutboxEntry, markOutboxEntrySending, markOutboxEntrySent, and markOutboxEntryFailed fileciteturn0file0turn0file2turn0file6.
services/api/src/modules/notifications/notification.service.js resolves the recipient, builds the message, sends it, and updates outbox status. This is the delivery side of the flow .
services/api/src/models/NotificationOutbox.js defines the notification event enum that this module reexports through EVENT_TYPES. The tests confirm the enum includes lifecycle and digest events such as PASSWORD_RESET_REQUESTED, EMAIL_VERIFICATION_REQUESTED, WELCOME, and LISTING_PERFORMANCE_DIGEST .
services/api/src/modules/auth/auth.service.js uses this module after lifecycle changes. The backend documentation also describes that the control layer does not import the email provider directly fileciteturn0file14turn0file17.
services/api/src/scripts/sendListingPerformanceDigests.js imports EVENT_TYPES and emitNotificationEvent for the digest workflow. That shows this module is shared by scheduled notification jobs, not only account lifecycle flows .