Skip to content

Notification Service API

Current Endpoints

  • GET /healthz
  • GET /readyz
  • GET /v1
  • GET /v1/admin/support/notification-summary
  • PUT /v1/notifications/{notificationId}/snapshot
  • GET /v1/notifications
  • POST /v1/notifications
  • POST /v1/notifications/batch
  • POST /v1/notifications/read-all
  • POST /v1/notifications/delete-all
  • GET /v1/notifications/{notificationId}
  • POST /v1/notifications/{notificationId}/read
  • POST /v1/notifications/{notificationId}/delete
  • GET /v1/notifications/preferences
  • PATCH /v1/notifications/preferences
  • GET /v1/parent/alerts
  • POST /v1/parent/alerts/{notificationId}/read
  • POST /v1/parent/alerts/read-all
  • POST /v1/alerts/weak-topic
  • POST /v1/events/notification
  • POST /v1/events/weak-topic-alert
  • GET /v1/delivery-attempts
  • POST /v1/delivery-attempts

Native Notification Foundation

Phase 9 starts with service-owned in-app notifications, preferences, parent alerts, and a small internal event/batch creation contract. Public /api/notifications*, /api/parent/alerts*, /api/alerts*, and /api/admin/inbox* remain legacy-proxied until gateway adapters enforce auth, recipient resolution, and legacy response compatibility.

Legacy evidence:

  • node-platform/apps/api/src/modules/notifications/notifications.controller.ts:11-127 maps notification inbox, parent alerts, weak-topic alert, and preferences routes.
  • node-platform/apps/api/src/modules/notifications/notification.service.ts:6-50 delegates notification operations to AppDataService.
  • node-platform/apps/api/src/modules/app-data/app-data.notifications.ts:21-228 implements inbox paging, filters, summary counts, mark-read, delete, parent alerts, weak-topic fanout, generic create, preferences, and preference gating.
  • node-platform/apps/api/prisma/schema.prisma:2703-2748 defines Notification, NotificationPreference, and NotificationEvent.
  • node-platform/apps/api/prisma/schema.prisma:3160-3167 defines notification event types.
  • node-platform/apps/api/src/modules/admin/inbox.controller.ts:32-146 creates admin broadcast notifications/events/audit logs.
  • node-platform/apps/api/src/modules/app-data/app-data.classrooms-engagement.ts:511-583 creates classroom notification fanout and notification events.
  • node-platform/apps/api/src/modules/app-data/app-data.exam-runtime-core.ts:807-925 creates exam assigned, grade released, and attempt submitted notifications.
  • node-platform/apps/api/src/modules/support/support.service.ts:126-158 and node-platform/apps/api/src/modules/support/support.service.ts:351-361 create support ticket events and admin notifications.
  • node-platform/apps/web/components/layout/app-shell.tsx:1094-1174 consumes /api/notifications for the notification bell.
  • node-platform/apps/web/components/inbox/inbox-client.tsx:178-262 consumes inbox paging, mark-read, and read-all routes.
  • node-platform/apps/web/components/admin/admin-inbox-client.tsx:143-225 consumes admin inbox paging, user filtering, read/delete, and broadcast routes.
  • node-platform/apps/web/app/parent/alerts/page.tsx:92-123 consumes parent alert list/read/read-all.
  • node-platform/apps/web/components/account/profile-settings-client.tsx:313-315 and node-platform/apps/web/components/account/profile-settings-client.tsx:603-607 consume notification preferences at /notifications/preferences.

Native contract:

  • GET /v1/notifications supports userId, organizationId, filter=ALL|UNREAD|READ|SUPPORT, q, page, and limit.
  • Non-admin callers are scoped to X-Actor-Id; admin callers may pass userId or list all service-owned notifications.
  • X-Organization-Id is used when creating rows if the input omits organizationId. It does not automatically filter inbox list/read-all/delete-all because legacy inbox behavior scopes primarily by userId and older rows can have organizationId = null.
  • POST /v1/notifications creates a single notification for an already-resolved recipient. The service stores ids only and does not query profile-service.
  • POST /v1/notifications/batch creates many notifications for already-resolved recipients. This is the native equivalent of legacy createMany fanout.
  • POST /v1/events/notification records NotificationEvent rows and can optionally create notifications in the same request.
  • POST /v1/events/notification is internal-only: it verifies X-Internal-Service and X-Internal-Token before decoding the payload. Valid transport senders are admin-service, attempt-service, classroom-service, and exam-service; after authentication, the body sourceService must match that canonical sender before persistence, so one allowed producer cannot write another producer's replay namespace. Each uses its service-specific token when configured, otherwise INTERNAL_SERVICE_TOKEN.
  • Event producers may pass sourceService and sourceEventId together. The pair is replay-idempotent: re-sending the same producer event returns the original event and does not duplicate inbox rows.
  • Event producers may pass preferenceType to apply a shared preference key before fanout. If it is omitted, each notification's type is used as the preference key.
  • Source-aware POST /v1/notifications/batch requests route through the same event fanout path when event.sourceService and event.sourceEventId are present.
  • Exam-service native assignment is a current producer: when configured with NOTIFICATION_SERVICE_URL and supplied resolved recipients, it sends exam.assigned.notification.v1 events with preferenceType=EXAM_ASSIGNED or EXAM_ASSIGNED_PARENT.
  • Classroom-service native quick notification is a current producer: when configured with NOTIFICATION_SERVICE_URL, it sends classroom.notification.sent.v1 events with preferenceType=CLASSROOM_NOTIFICATION for current classroom STUDENT member ids. When classroom-service also has PROFILE_SERVICE_URL, it resolves parent account ids through profile-service and sends separate classroom.notification.parent.sent.v1 events with preferenceType=CLASSROOM_NOTIFICATION_PARENT.
  • Attempt-service native submit/grade is a current producer: when configured with NOTIFICATION_SERVICE_URL, it sends attempt.grade_released.notification.v1 events only after the result is visible to the student. It uses preferenceType=GRADE_RELEASED for the student and GRADE_RELEASED_PARENT for parents resolved through profile-service /v1/internal/profiles/parent-recipients/resolve.
  • Admin-service native explicit-recipient broadcast is a current producer: when configured with NOTIFICATION_SERVICE_URL, it sends admin.notification.broadcast.v1 events with sourceEventId=admin:broadcast:{clientRequestId} and preferenceType=ADMIN_BROADCAST unless a custom notification type is supplied. Admin-service does not resolve legacy ALL or ROLES targets.
  • POST /v1/events/weak-topic-alert is the bounded internal adapter for analytics-produced weak-topic parent alert intents. It requires sourceService, sourceEventId, studentId, topic, and at least one already-resolved parent recipient in parentIds or recipientIds.
  • Weak-topic requests are transported by bff-service, which must send X-Internal-Service: bff-service and X-Internal-Token; the payload can still retain sourceService=analytics-service for domain replay attribution.
  • The weak-topic event adapter creates WEAK_TOPIC_ALERT rows with the legacy title/content shape, applies WEAK_TOPIC_ALERT preferences and mutedUntil before persisting inbox rows, and uses the event source store so redelivery returns created=0 without duplicating parent alerts.
  • Notification-service does not resolve parent/student/classroom relationships for weak-topic events and does not query profile-service or classroom-service databases. Parent recipient ids must be supplied by analytics/profile/gateway adapters.
  • Notification-service resolves NOTIFICATION_INTERNAL_SERVICE_TOKEN before shared INTERNAL_SERVICE_TOKEN; local/test uses the repository local token. Startup rejects production-like environments without either token, while a manually constructed server with no token returns 503 NOTIFICATION_INTERNAL_TOKEN_NOT_CONFIGURED. Missing, wrong, or unsupported sender credentials return 401 NOTIFICATION_INTERNAL_UNAUTHORIZED before JSON decoding or mutation.
  • When AUDIT_SERVICE_URL or AUDIT_SERVICE_BASE_URL is configured, newly persisted notification events are projected to audit-service /v1/audit-events with sourceService=notification-service, sourceEventId=<notification event id>, category=DOMAIN, severity=INFO, metadataSchemaVersion=1, retentionPolicy=notification-events, and createdAt -> occurredAt. Source event replay returns the existing notification event and does not publish another audit row.
  • POST /v1/delivery-attempts records provider delivery attempt logs for already-created notification ids. Required fields are notificationId, userId, channel, provider, and status=QUEUED|SENT|FAILED|DEAD_LETTERED; attempt defaults to 1.
  • GET /v1/delivery-attempts supports notificationId, organizationId, userId, channel, provider, status, from, to, page, and limit. Admin callers may inspect all attempts; non-admin callers are scoped to X-Actor-Id.
  • Delivery attempt rows are internal diagnostics/retry metadata. They do not deliver email/SMS/push by themselves and do not change inbox read/delete state.
  • GET /v1/admin/support/notification-summary is the owner projection for admin-service support.queues source snapshots. It summarizes support notification rows, support notification events, and delivery attempt state tied to support notification rows only. It does not store support tickets, discover recipients, or replace admin audit history.
  • Mark-read and delete routes preserve legacy ownership behavior: admin may operate globally, non-admin must own the notification.
  • Preferences default to enabledTypes={} and therefore allow all notification types unless a type is explicitly set to false.
  • mutedUntil suppresses future event fanout but does not hide already-created notifications.
  • Parent alerts are the same notification rows scoped to the parent actor; frontend category filtering remains type-name based.
  • Weak-topic alert creation accepts resolved parent ids in native mode. Legacy still resolves ParentStudent links; native user relationship lookup belongs to profile-service/gateway adapters.

Known compatibility note:

  • Legacy controller exposes preferences under GET/PATCH /api/alerts/preferences, while current profile UI calls GET/PATCH /api/notifications/preferences. P9-001 exposes native /v1/notifications/preferences; gateway compatibility work must decide whether to preserve both public aliases.

Database:

  • services/notification-service/migrations/000002_notifications.sql creates notifications, notification_preferences, and notification_events.
  • services/notification-service/migrations/000003_notification_event_sources.sql adds source replay metadata and a partial unique index on (source_service, source_event_id).
  • services/notification-service/migrations/000004_notification_delivery_attempts.sql creates notification_delivery_attempts for provider attempt status, retry timing, provider ids, error text, and metadata.
  • Cross-service ids such as organization_id, user_id, actor_id, target_user_id, entity, and entity_id are stored as public ids only.
  • Admin audit logs stay admin-service owned. Notification events and delivery attempts are service-local inputs/diagnostics, not a replacement for admin audit history.

Rollback:

  • Keep /api/notifications*, /api/parent/alerts*, /api/alerts*, and /api/admin/inbox* routed to legacy.
  • Disable gateway callers for /v1/notifications*, /v1/parent/alerts*, /v1/alerts*, /v1/events/notification, /v1/events/weak-topic-alert, /v1/delivery-attempts, and /v1/admin/support/notification-summary.
  • Drop notification-service local tables with the migration down step if local test data must be reset.

Gateway rehearsal:

deploy/gateway/routes.notifications-native-example.json and deploy/gateway/routes.notifications-native-localhost-example.json provide non-default route-table rehearsals for inbox and preference paths. They route only:

  • GET /api/notifications
  • GET /api/notifications/{notificationId}
  • POST /api/notifications/{notificationId}/read
  • POST /api/notifications/{notificationId}/delete
  • POST /api/notifications/read-all
  • POST /api/notifications/delete-all
  • GET /api/notifications/preferences
  • PATCH /api/notifications/preferences
  • GET /api/alerts/preferences
  • PATCH /api/alerts/preferences

to notification-service with gateway actor header injection. The route table does not require organization at the gateway because current shell notification fetches can send only Authorization. Notification create/batch/snapshot, parent alerts, weak-topic alerts, admin inbox, and broad notification/alert routes stay legacy-proxied until recipient resolution, support-ticket, role broadcast, and admin-audit adapters are explicit.

Run make test-notification-routes before any live or browser rehearsal. The QA handoff lives in docs/qa/notification-route-rehearsals.md.

Parent-alert gateway rehearsal:

deploy/gateway/routes.parent-alerts-native-example.json and deploy/gateway/routes.parent-alerts-native-localhost-example.json provide non-default route-table rehearsals for the parent-facing alert read surface. They route only:

  • GET /api/parent/alerts
  • POST /api/parent/alerts/{notificationId}/read
  • POST /api/parent/alerts/read-all

to notification-service with gateway actor header injection and a global PARENT role check. The route table does not require organization at the gateway because the parent alert UI is current-parent scoped. Weak-topic alert creation, parent-child relationship lookup, broad notification/alert/admin routes, and fallback stay legacy-proxied until recipient resolution and cross-service relationship adapters are explicit.

Run make test-parent-alert-routes before any live or browser rehearsal. The QA handoff lives in docs/qa/parent-alert-route-rehearsals.md.

Non-goals for P9-001:

  • Public gateway adapter/cutover.
  • Live Email/SMS/push provider integration.
  • Recipient search and role broadcast resolution from profile-service.
  • Admin audit-log ownership.
  • Support ticket storage.
  • Analytics dashboards.

Go-platform documentation is generated from repository Markdown.