Supercommerce API Docs
Full Module Docs

Notifications Module

HTTP surface for the event-driven notification system — customer + vendor mobile-device registration for FCM push, admin broadcasts (one-off email or push to a defined audience),…

HTTP surface for the event-driven notification system — customer + vendor mobile-device registration for FCM push, admin broadcasts (one-off email or push to a defined audience), and the admin notification audit log.

Source: api-modules/notifications (registered via NotificationsModule.forRoot({ providers, templates, resolvers }) in apps/api/src/app.module.ts).

The module is a router, not a sender. It listens for domain events (order.placed, order.vendor.fulfilled, etc.) via NestJS @OnEvent, resolves recipients + channels via per-event resolvers, renders templates, and dispatches to provider modules. Concrete senders live in:

  • api-modules/notifications-email-mailer — SMTP email via the mailer module
  • api-modules/notifications-push-fcm — Firebase Cloud Messaging push

Both register NotificationProvider instances against NOTIFICATION_PROVIDERS. Adding a new channel (SMS, WhatsApp) is a new provider module — the core notifications module doesn't change.


Conventions

Authentication

Endpoint groupAuthPermission
POST /store/devices, DELETE /store/devicesrequired (customer)
POST /vendor/devices, DELETE /vendor/devicesrequired (vendor session)
POST /admin/notifications/broadcasts, POST /admin/notifications/broadcasts/:id/cancelrequirednotifications: broadcast
GET /admin/notifications/broadcasts, GET /admin/notifications/broadcasts/:id, GET /admin/notifications/logrequirednotifications: view

Response envelope

Successful responses are wrapped by ResponseInterceptor:

{
  "data": <payload>,
  "message": "Success",
  "statusCode": 200,
  "metadata": { /* optional, e.g. pagination */ }
}

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR
401UNAUTHORIZED
403FORBIDDEN
404NOT_FOUND
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Architecture

┌──────────────────────┐   EventEmitter2   ┌────────────────────────────────┐
│ order / shipping /   │ ──────────────▶  │ NotificationOrchestratorService │
│ catalog / cart …     │                  │ (listens via @OnEvent)          │
└──────────────────────┘                  └─────────────────┬──────────────┘

                            ┌───────────────────────────────┼──────────────────────┐
                            ▼                               ▼                      ▼
                  RecipientResolver           NotificationTemplate        NotificationProvider
                  (per-event class)           (per-(event, channel))      (email, push, ...)
                            │                               │                      │
                            └───── BullMQ "notification" ───┴──── enqueue ─────────┘


                                       NotificationLog
                                       (audit row per send)

Admin broadcast bypasses the resolver path: the controller writes a broadcast row, enqueues onto the notification.broadcast queue, and the worker fans out per-recipient notification jobs based on the chosen audience.


Domain types

DeviceResponse

type DeviceResponse = {
  id: string;
  platform: "android" | "ios";
  appKind: "customer" | "vendor";
  lastSeenAt: string;            // ISO; updated on every re-register
};

BroadcastResponse

type BroadcastResponse = {
  id: string;
  status: string;                // "scheduled" | "running" | "completed" | "cancelled" | "failed"
  audienceType: string;          // "all_customers" | "all_vendors" | "user_ids"
  channels: string[];            // subset of ["email", "push"]
  scheduledFor: string | null;   // ISO; null = send-now
  totalRecipients: number | null;
  sentCount: number;
  failedCount: number;
  createdAt: string;
};

NotificationLogResponse

One row per send attempt (success or failure). Use this to answer "did the customer get the email?" from support.

type NotificationLogResponse = {
  id: string;
  recipientUserId: string | null;     // null when send was to a non-user (rare)
  channel: string;                    // "email" | "push"
  provider: string;                   // e.g. "smtp-mailer", "fcm"
  eventType: string;                  // e.g. "order.placed", "admin.broadcast"
  broadcastId: string | null;         // populated for broadcast sends
  subject: string | null;             // email subject (or push title for "push")
  status: string;                     // "sent" | "failed"
  error: string | null;
  sentAt: string;                     // ISO
};

Devices

Both the customer and vendor app register their FCM token after login. Re-registering the same token is idempotent — it refreshes lastSeenAt instead of creating a duplicate row. Apps are expected to call DELETE on logout to stop unrelated pushes from leaking to a shared device.

POST /store/devices — Register customer device

Body

{
  "platform": "android",          // "android" | "ios"
  "token": "fcm-registration-token-..." // 10..4096 chars
}

Response 201DeviceResponse with appKind="customer".


DELETE /store/devices — Unregister customer device

Body

{ "token": "fcm-registration-token-..." }

Idempotent — unknown token is a no-op 200.

Response 200

{ "data": { "ok": true }, "message": "Success", "statusCode": 200 }

POST /vendor/devices — Register vendor device

Same shape and rules as the storefront version, scoped to the vendor session. Returns DeviceResponse with appKind="vendor".

DELETE /vendor/devices — Unregister vendor device

Same shape as storefront.


Admin broadcasts

Base path: /admin/notifications/broadcasts.

POST /admin/notifications/broadcasts — Create a broadcast

Required permission: notifications: broadcast. Send-now (omit scheduleFor) or scheduled (future ISO datetime).

Body

{
  "audience": {
    "type": "user_ids",
    "userIds": ["01J9...", "01J9..."]   // 1..10_000
  },
  "channels": ["email", "push"],         // 1..2 of "email" | "push"
  "content": {
    "email": {
      "subject": "Festive sale starts tomorrow",
      "html": "<h1>Save 30%</h1>...",
      "text": "Save 30%..."             // optional plaintext fallback
    },
    "push": {
      "title": "Festive sale",
      "body":  "Up to 30% off — starts in 24h",
      "imageUrl": "https://cdn.example/banner.png",
      "data": { "deepLink": "/sale" }
    }
  },
  "scheduleFor": "2026-05-12T03:00:00.000Z"   // optional; past values reject
}

audience.type discriminator (mutually exclusive):

TypeExtra fieldsResolved audience
all_customersevery user with appKind="customer" device or email
all_vendorsevery user with appKind="vendor" device or email
user_idsuserIds: string[] (1..10_000)the listed ids

Cross-field rule: content.email is required when channels contains "email"; content.push is required when channels contains "push".

Response 201BroadcastResponse. status reflects whether it's scheduled or already running.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod; missing content for a listed channel; scheduleFor in the past
403FORBIDDENCaller lacks notifications: broadcast

GET /admin/notifications/broadcasts — List broadcasts

Required permission: notifications: view. Newest first.

Query

NameTypeDefaultConstraints
pageint1>= 1
limitint501..200
statusstring?filter by status

Response 200 — paginated envelope of BroadcastResponse[].


GET /admin/notifications/broadcasts/:id — Broadcast detail

Required permission: notifications: view. Returns the BroadcastResponse with current counters (sentCount, failedCount).


POST /admin/notifications/broadcasts/:id/cancel — Cancel a broadcast

Required permission: notifications: broadcast.

ScenarioBehavior
Scheduled (not yet started)Removes the delayed start job from BullMQ; marks the row cancelled
In-progress (start job has run, per-recipient jobs are still queued)Marks the row cancelled. Per-recipient jobs are still popped but skip when they see the cancelled flag. In-flight provider calls (currently uploading to FCM, currently in SMTP RCPT) are not aborted
Already terminal (completed, failed, cancelled)No-op 200

Response 200BroadcastResponse.


Admin log

GET /admin/notifications/log — Audit log

Required permission: notifications: view. One row per send attempt. Newest first.

Query

NameTypeDefaultConstraints
pageint1>= 1
limitint501..200
recipientUserIdstring?filter to one user (support flow)
eventTypestring?e.g. "order.placed", "admin.broadcast"
broadcastIdstring?every send from one broadcast

eventType="admin.broadcast" is the sentinel for sends from the broadcast pathway; per-event sends use the underlying domain event name.

Response 200 — paginated envelope of NotificationLogResponse[].


Event-driven sends (no HTTP surface)

The orchestrator subscribes to these events out of the box (see resolvers/order/). The set is extensible — add a RecipientResolver + NotificationTemplate (+ register them in NotificationsModule.forRoot) for any new event.

EventRecipientsChannelsTemplate summary
order.placedcustomer, every vendor on the orderemail + push"Your order #N is placed" / "New order on <product>"
order.paidcustomeremail"Payment received for order #N"
order.vendor.fulfilledcustomeremail + push"Your order is on the way" with tracking
order.vendor.deliveredcustomeremail + push"Order delivered"
order.vendor.cancelled / order.cancelledcustomer (+ vendor when admin-initiated)email"Your order was cancelled"
shipping.tracking.updated (out_for_delivery)customerpush"Out for delivery — arriving today"

Recipient resolution falls back gracefully — a customer with no FCM device gets email-only; one with no email skips the email channel. Send failures land in notification_log with status="failed" and the provider error.


Configuration

Process env (used by provider modules)

Env varModulePurpose
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROMmailer / notifications-email-mailerSMTP credentials + From address for outbound email
FCM_PROJECT_ID, FCM_CLIENT_EMAIL, FCM_PRIVATE_KEYnotifications-push-fcmFirebase service-account credentials for push delivery

Constants

NameValuePurpose
NOTIFICATION_QUEUE"notification"BullMQ queue for per-recipient dispatch jobs
NOTIFICATION_BROADCAST_QUEUE"notification.broadcast"BullMQ queue for broadcast start + fan-out jobs
ADMIN_BROADCAST_EVENT_TYPE"admin.broadcast"Sentinel event_type on log rows from a broadcast

  • notifications-email-mailer — SMTP email provider. Wraps @sc/mailer.
  • notifications-push-fcm — FCM push provider.
  • mailer — low-level SMTP transport.
  • queue — BullMQ infrastructure shared with inventory and other async workers.
  • order / shipping — primary event sources today. See order.md, shipping.md.

On this page