Appearance
Notification Service API
Current Endpoints
GET /healthzGET /readyzGET /v1GET /v1/admin/support/notification-summaryPUT /v1/notifications/{notificationId}/snapshotGET /v1/notificationsPOST /v1/notificationsPOST /v1/notifications/batchPOST /v1/notifications/read-allPOST /v1/notifications/delete-allGET /v1/notifications/{notificationId}POST /v1/notifications/{notificationId}/readPOST /v1/notifications/{notificationId}/deleteGET /v1/notifications/preferencesPATCH /v1/notifications/preferencesGET /v1/parent/alertsPOST /v1/parent/alerts/{notificationId}/readPOST /v1/parent/alerts/read-allPOST /v1/alerts/weak-topicPOST /v1/events/notificationPOST /v1/events/weak-topic-alertGET /v1/delivery-attemptsPOST /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-127maps notification inbox, parent alerts, weak-topic alert, and preferences routes.node-platform/apps/api/src/modules/notifications/notification.service.ts:6-50delegates notification operations toAppDataService.node-platform/apps/api/src/modules/app-data/app-data.notifications.ts:21-228implements 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-2748definesNotification,NotificationPreference, andNotificationEvent.node-platform/apps/api/prisma/schema.prisma:3160-3167defines notification event types.node-platform/apps/api/src/modules/admin/inbox.controller.ts:32-146creates admin broadcast notifications/events/audit logs.node-platform/apps/api/src/modules/app-data/app-data.classrooms-engagement.ts:511-583creates classroom notification fanout and notification events.node-platform/apps/api/src/modules/app-data/app-data.exam-runtime-core.ts:807-925creates exam assigned, grade released, and attempt submitted notifications.node-platform/apps/api/src/modules/support/support.service.ts:126-158andnode-platform/apps/api/src/modules/support/support.service.ts:351-361create support ticket events and admin notifications.node-platform/apps/web/components/layout/app-shell.tsx:1094-1174consumes/api/notificationsfor the notification bell.node-platform/apps/web/components/inbox/inbox-client.tsx:178-262consumes inbox paging, mark-read, and read-all routes.node-platform/apps/web/components/admin/admin-inbox-client.tsx:143-225consumes admin inbox paging, user filtering, read/delete, and broadcast routes.node-platform/apps/web/app/parent/alerts/page.tsx:92-123consumes parent alert list/read/read-all.node-platform/apps/web/components/account/profile-settings-client.tsx:313-315andnode-platform/apps/web/components/account/profile-settings-client.tsx:603-607consume notification preferences at/notifications/preferences.
Native contract:
GET /v1/notificationssupportsuserId,organizationId,filter=ALL|UNREAD|READ|SUPPORT,q,page, andlimit.- Non-admin callers are scoped to
X-Actor-Id; admin callers may passuserIdor list all service-owned notifications. X-Organization-Idis used when creating rows if the input omitsorganizationId. It does not automatically filter inbox list/read-all/delete-all because legacy inbox behavior scopes primarily byuserIdand older rows can haveorganizationId = null.POST /v1/notificationscreates a single notification for an already-resolved recipient. The service stores ids only and does not query profile-service.POST /v1/notifications/batchcreates many notifications for already-resolved recipients. This is the native equivalent of legacycreateManyfanout.POST /v1/events/notificationrecordsNotificationEventrows and can optionally create notifications in the same request.POST /v1/events/notificationis internal-only: it verifiesX-Internal-ServiceandX-Internal-Tokenbefore decoding the payload. Valid transport senders areadmin-service,attempt-service,classroom-service, andexam-service; after authentication, the bodysourceServicemust 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, otherwiseINTERNAL_SERVICE_TOKEN.- Event producers may pass
sourceServiceandsourceEventIdtogether. 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
preferenceTypeto apply a shared preference key before fanout. If it is omitted, each notification'stypeis used as the preference key. - Source-aware
POST /v1/notifications/batchrequests route through the same event fanout path whenevent.sourceServiceandevent.sourceEventIdare present. - Exam-service native assignment is a current producer: when configured with
NOTIFICATION_SERVICE_URLand supplied resolved recipients, it sendsexam.assigned.notification.v1events withpreferenceType=EXAM_ASSIGNEDorEXAM_ASSIGNED_PARENT. - Classroom-service native quick notification is a current producer: when configured with
NOTIFICATION_SERVICE_URL, it sendsclassroom.notification.sent.v1events withpreferenceType=CLASSROOM_NOTIFICATIONfor current classroomSTUDENTmember ids. When classroom-service also hasPROFILE_SERVICE_URL, it resolves parent account ids through profile-service and sends separateclassroom.notification.parent.sent.v1events withpreferenceType=CLASSROOM_NOTIFICATION_PARENT. - Attempt-service native submit/grade is a current producer: when configured with
NOTIFICATION_SERVICE_URL, it sendsattempt.grade_released.notification.v1events only after the result is visible to the student. It usespreferenceType=GRADE_RELEASEDfor the student andGRADE_RELEASED_PARENTfor 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 sendsadmin.notification.broadcast.v1events withsourceEventId=admin:broadcast:{clientRequestId}andpreferenceType=ADMIN_BROADCASTunless a custom notification type is supplied. Admin-service does not resolve legacyALLorROLEStargets. POST /v1/events/weak-topic-alertis the bounded internal adapter for analytics-produced weak-topic parent alert intents. It requiressourceService,sourceEventId,studentId,topic, and at least one already-resolved parent recipient inparentIdsorrecipientIds.- Weak-topic requests are transported by
bff-service, which must sendX-Internal-Service: bff-serviceandX-Internal-Token; the payload can still retainsourceService=analytics-servicefor domain replay attribution. - The weak-topic event adapter creates
WEAK_TOPIC_ALERTrows with the legacy title/content shape, appliesWEAK_TOPIC_ALERTpreferences andmutedUntilbefore persisting inbox rows, and uses the event source store so redelivery returnscreated=0without 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_TOKENbefore sharedINTERNAL_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 returns503 NOTIFICATION_INTERNAL_TOKEN_NOT_CONFIGURED. Missing, wrong, or unsupported sender credentials return401 NOTIFICATION_INTERNAL_UNAUTHORIZEDbefore JSON decoding or mutation. - When
AUDIT_SERVICE_URLorAUDIT_SERVICE_BASE_URLis configured, newly persisted notification events are projected to audit-service/v1/audit-eventswithsourceService=notification-service,sourceEventId=<notification event id>,category=DOMAIN,severity=INFO,metadataSchemaVersion=1,retentionPolicy=notification-events, andcreatedAt -> occurredAt. Source event replay returns the existing notification event and does not publish another audit row. POST /v1/delivery-attemptsrecords provider delivery attempt logs for already-created notification ids. Required fields arenotificationId,userId,channel,provider, andstatus=QUEUED|SENT|FAILED|DEAD_LETTERED;attemptdefaults to1.GET /v1/delivery-attemptssupportsnotificationId,organizationId,userId,channel,provider,status,from,to,page, andlimit. Admin callers may inspect all attempts; non-admin callers are scoped toX-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-summaryis the owner projection for admin-servicesupport.queuessource 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 tofalse. mutedUntilsuppresses 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
ParentStudentlinks; 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 callsGET/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.sqlcreatesnotifications,notification_preferences, andnotification_events.services/notification-service/migrations/000003_notification_event_sources.sqladds source replay metadata and a partial unique index on(source_service, source_event_id).services/notification-service/migrations/000004_notification_delivery_attempts.sqlcreatesnotification_delivery_attemptsfor 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, andentity_idare stored as public ids only. - Admin audit logs stay
admin-serviceowned. 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/notificationsGET /api/notifications/{notificationId}POST /api/notifications/{notificationId}/readPOST /api/notifications/{notificationId}/deletePOST /api/notifications/read-allPOST /api/notifications/delete-allGET /api/notifications/preferencesPATCH /api/notifications/preferencesGET /api/alerts/preferencesPATCH /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/alertsPOST /api/parent/alerts/{notificationId}/readPOST /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.