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 fileciteturn0file0turn0file2.

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 fileciteturn0file0turn0file2.

[!IMPORTANT] Notification failures are absorbed here. A failed delivery does not roll back the domain change that already persisted fileciteturn0file0turn0file2.

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 fileciteturn0file0turn0file9.

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 fileciteturn0file0turn0file2.

[!NOTE] The function does not call the email provider directly. That responsibility stays inside notification.service.js and notification.client.js fileciteturn0file0turn0file2turn0file6.

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 — Normalizes NOTIFICATION_EVENT_TYPES into a frozen lookup object.
  • emitNotificationEvent — Persists a notification outbox entry, tries to deliver it, and returns the entry or null.
  • emitNotificationEvent — Logs OUTBOX_PERSISTENCE_FAILED when outbox creation throws.
  • emitNotificationEvent — Logs EMAIL_DELIVERY_FAILED when 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
  1. Call emitNotificationEvent | The caller passes the event name and optional recipient data.
  2. Persist the outbox entry | createOutboxEntry writes the event to NotificationOutbox.
  3. Attempt delivery | deliverOutboxEntry tries to send the notification immediately.
  4. Return the result | Persistence failure returns null; delivery failure still returns the stored entry.

Gotchas & edge cases

[!WARNING] emitNotificationEvent only returns null when outbox persistence fails. Delivery errors are absorbed and logged instead fileciteturn0file0turn0file2.

[!WARNING] This module does not validate delivery support itself. notification.service.js later rejects unsupported recipient channels such as anything other than primary_email with a UNSUPPORTED_RECIPIENT_CHANNEL error .

[!NOTE] The module is used after domain writes. The auth lifecycle calls emitNotificationEvent after persisting state changes, and the digest job also imports it for scheduled mail flows fileciteturn0file14turn0file3.

services/api/src/modules/notifications/notification.repository.js stores the outbox entry and updates its status. It owns createOutboxEntry, markOutboxEntrySending, markOutboxEntrySent, and markOutboxEntryFailed fileciteturn0file0turn0file2turn0file6.

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 fileciteturn0file14turn0file17.

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 .