BIO.RE
Notifications

Broadcast (admin) — Submit, queue, approve, reject

Four admin endpoints for sending platform-wide or segmented broadcasts (e.g. system_maintenance, marketing_campaign). Behind a two-person approval gate in production. Submit → pending queue → second admin approves or rejects.

/api/v1/admin/notifications/broadcast* — 👤 Admin · permission notification:manage

Wave F-3 (shipped 2026-06-22) added a two-person approval gate for broadcast sends. When the notification.broadcast_dual_approval flag is TRUE (production default), every broadcast lands in NotificationBroadcastRequest as PENDING and a second admin must call the approve endpoint to fire the segment fan-out. Same-admin guard: the requester cannot approve or reject their own pending request.

Response shape varies by flag. When notification.broadcast_dual_approval is TRUE, POST /broadcast returns { requestId, status: 'PENDING' }. When FALSE (dev/staging), it falls back to the legacy direct-send path and returns { sent, total, segment, eventKey }. Clients should narrow on the presence of requestId vs sent. See BroadcastRequestResponseDto.

Approval pattern mirrors PayoutRequest. Atomic flip via updateMany WHERE requesterId != approverId — race-safe against double-approve and self-approval (apps/api-core/src/modules/payout/payout.service.ts:147-200 is the source pattern). PENDING requests older than retention.broadcast_request_ttl_hours (default 24h) are swept to EXPIRED by the daily retention cron.

POST /admin/notifications/broadcast — submit

Submits a broadcast for the configured segment. Returns either a pending request ID (flag ON) or direct fan-out result (flag OFF).

Request body — RequestBroadcastDto

FieldTypeRequiredValidationNotes
eventKeystringIsString, IsNotEmpty, MaxLength(128)Must exist in NOTIFICATION_EVENTS. Common: marketing_campaign, system_maintenance, broadcast_approved.
segmentenumIsIn(['all', 'creators', 'fans'])all = active users (cap 10k); creators = active users with a CreatorProfile; fans = active users without a CreatorProfile.
variablesobjectIsObjectTemplate variables interpolated into subject / body. Stringified at send time.
channelsobjectnested BroadcastChannelMaskDto{ email?, push?, inApp?, sms? } booleans. Omitted flags default false except inApp which defaults true.
scheduledAtstringIsISO8601Future ISO timestamp. Past or null fires immediately. Propagated onto every per-user NotificationJob.scheduleAt.

Throttle / permission

LayerLimit
Permissionnotification:manage
Throttle5 req / hour
HTTP code200

Response — flag ON

{
  "success": true,
  "data": {
    "requestId": "n-broadcast-uuid-here",
    "status": "PENDING"
  }
}

Response — flag OFF (dev / staging)

{
  "success": true,
  "data": {
    "sent": 4823,
    "total": 4823,
    "segment": "all",
    "eventKey": "system_maintenance"
  }
}

Errors

HTTPReason
400Invalid payload (bad segment enum, missing eventKey, etc.)
401Missing / invalid admin session
403Permission denied (notification:manage required)

GET /admin/notifications/broadcast/pending — pending queue

Returns up to 100 NotificationBroadcastRequest rows in PENDING status, oldest first. Powers the admin queue view for second-admin sign-off.

Response — ArrayApiResponseOf<PendingBroadcastItemDto>

{
  "success": true,
  "data": [
    {
      "id": "n-broadcast-uuid-here",
      "eventKey": "marketing_campaign",
      "segment": "fans",
      "channels": { "email": true, "push": false, "inApp": true, "sms": false },
      "variables": { "campaignName": "Spring 2026" },
      "scheduledAt": null,
      "requesterId": "admin-uuid",
      "requestedAt": "2026-06-22T14:00:00.000Z"
    }
  ]
}
FieldTypeNotes
idstring (UUID)NotificationBroadcastRequest.id — used in the approve / reject path
eventKeystringFrom submit payload
segmentenum'all' | 'creators' | 'fans'
channelsobjectChannel mask submitted by the requester
variablesobject | nullTemplate variables submitted by the requester
scheduledAtstring | nullISO 8601 if scheduled, null for immediate-fire
requesterIdstring (UUID)Admin who created the request — same-admin guard uses this
requestedAtstring (ISO 8601)Creation timestamp

Errors

HTTPReason
401Missing / invalid admin session
403Permission denied (read scope = class-level notification:read)

POST /admin/notifications/broadcast/:id/approve

Second admin approves a pending broadcast. Atomic updateMany WHERE requesterId != approverId AND status = PENDING prevents self-approval and double-processing. On success, segment fan-out runs inline and NotificationBroadcastRequest.status flips PENDING → SENT.

Throttle / permission

LayerLimit
Permissionnotification:manage
Throttle20 req / hour
HTTP code200

Response — BroadcastApprovalResponseDto

{
  "success": true,
  "data": {
    "requestId": "n-broadcast-uuid-here",
    "status": "SENT",
    "sent": 4823,
    "total": 4823,
    "segment": "fans",
    "eventKey": "marketing_campaign"
  }
}

Errors

HTTPReason
400Cannot self-approve OR already processed (admin.notification.broadcast_self_approve / broadcast_invalid_status)
401Missing / invalid admin session
403Permission denied (notification:manage required)
404Broadcast request not found

Side effects

  1. Atomic flip PENDING → APPROVED with approverId + approvedAt set (race-safe).
  2. If matched row is 0, disambiguates: findUnique → 404 (missing) / 400 self-approve / 400 wrong-status.
  3. Load the now-APPROVED row, fan out via fanOutBroadcast using requestId as the dedupe seed (stable across the request → approve gap — re-approve never double-sends).
  4. scheduledAt in the future is propagated onto every per-user NotificationJob.scheduleAt so the worker poll respects the slot.
  5. Flip APPROVED → SENT with sentAt timestamp.
  6. Fire broadcast_approved in-app notification to the requester (fire-and-forget, no email).

POST /admin/notifications/broadcast/:id/reject

Second admin rejects a pending broadcast. reason is required (10–500 chars) and persisted on the request row for audit.

Request body — RejectBroadcastDto

FieldTypeRequiredValidationNotes
reasonstringMinLength(10), MaxLength(500)Persisted as rejectionReason. Trimmed and clipped to 500 chars server-side.

Throttle / permission

LayerLimit
Permissionnotification:manage
Throttle20 req / hour
HTTP code200

Response — BroadcastRejectionResponseDto

{
  "success": true,
  "data": {
    "requestId": "n-broadcast-uuid-here",
    "status": "REJECTED"
  }
}

Errors

HTTPReason
400Reason empty / too short / already processed / self-rejection attempted
401Missing / invalid admin session
403Permission denied (notification:manage required)
404Broadcast request not found

Side effects

  1. Validate reason (non-empty after trim — 400 if blank, length validators run at DTO level).
  2. Atomic flip PENDING → REJECTED with approverId, approvedAt, rejectionReason set (race-safe; same-admin guard).
  3. If matched is 0, disambiguates: 404 (missing) / 400 self-rejection / 400 wrong-status.
  4. No notification dispatch to the requester (advisor scope-cut for F3).

Source

SourcePathLines
Controller (POST /broadcast)apps/api-core/src/modules/notification/admin-notification.controller.ts645–675
Controller (GET /broadcast/pending)apps/api-core/src/modules/notification/admin-notification.controller.ts677–692
Controller (POST /broadcast/:id/approve)apps/api-core/src/modules/notification/admin-notification.controller.ts694–717
Controller (POST /broadcast/:id/reject)apps/api-core/src/modules/notification/admin-notification.controller.ts719–743
Request DTO (submit)apps/api-core/src/modules/notification/dto/admin-notification.dto.ts275–309 (RequestBroadcastDto)
Request DTO (reject)apps/api-core/src/modules/notification/dto/admin-notification.dto.ts315–325 (RejectBroadcastDto)
Response DTO (submit)apps/api-core/src/modules/notification/dto/admin-notification-response.dto.ts358–367 (BroadcastRequestResponseDto)
Response DTO (approve)apps/api-core/src/modules/notification/dto/admin-notification-response.dto.ts375–393 (BroadcastApprovalResponseDto)
Response DTO (reject)apps/api-core/src/modules/notification/dto/admin-notification-response.dto.ts399–405 (BroadcastRejectionResponseDto)
Response DTO (pending item)apps/api-core/src/modules/notification/dto/admin-notification-response.dto.ts410–441 (PendingBroadcastItemDto)
Service (broadcast router)apps/api-core/src/modules/notification/admin-notification.service.ts1335–1364
Service (approveBroadcast)apps/api-core/src/modules/notification/admin-notification.service.ts1066–1198
Service (rejectBroadcast)apps/api-core/src/modules/notification/admin-notification.service.ts1206–1256
Service (listPendingBroadcasts)apps/api-core/src/modules/notification/admin-notification.service.ts1263–1293
Service (expireStaleBroadcasts — cron sweep)apps/api-core/src/modules/notification/admin-notification.service.ts1305–1324
Prisma modelpackages/prisma/prisma/schema.prismaNotificationBroadcastRequest (2259), BroadcastRequestStatus enum (2280)
Migrationpackages/prisma/prisma/migrations/20260622000002_notification_broadcast_request/migration.sql(model + enum + indexes)
Live responseNOT verified — sourced from DTOs cited above

On this page