Notification System
Real-time, multi-channel notifications for both frontend and admin apps. Features:
- Live updates via Server-Sent Events (SSE) with unread count fan-out.
- Delivery strategy pipeline with email fallback for offline users (push, not implemented, reserved for future).
- Per-user preferences (email/push toggles and per-type controls).
- Actionable notifications with domain allowlisting to prevent unsafe redirects.
- Admin broadcast tool for targeted or global sends.
Architecture at a Glance
- Source of truth:
notification+notification_preferencestables. - Service layer:
apps/api/src/lib/notification-service.tshandles creation, unread caching (Redis), pagination, and broadcasts. - Delivery strategies (ordered by
DELIVERY_STRATEGY_PRIORITYinpackages/shared/config/notification.ts):sse— immediate delivery to connected users vianotification-sse-manager.email— Nodemailer-based fallback when users are offline and have email enabled.push— placeholder for future expansion.
- Transport: SSE endpoint
/notification/stream(cookie-authenticated). Optional heartbeat/notification/heartbeatcan be used by clients. - Distribution: When
ENABLE_DISTRIBUTED_CHATis enabled, notification pub/sub piggybacks on Redis to fan out SSE events across instances. - Caching: Unread counts cached in Redis (
notification:unread:${userId}) with TTL fromNOTIFICATION_CONFIG.unreadCountCacheTTL.
Data Model
notificationid,userId,type(NotificationTypeenum),title,contentmetadataJSON (actions, priority, arbitrary data)readflag, timestamps
notification_preferencesemailEnabled,pushEnabledemailTypes,pushTypes(per-type delivery toggles)- timestamps
API Surface (all routes under /notification, auth required)
| Method & Path | Description |
|---|---|
GET /stream | SSE stream. Emits connected, new_notification, notification_updated, notification_deleted, unread_count_changed, keep_alive, error. |
POST /heartbeat | Optional client heartbeat to keep presence fresh. |
GET /list | Paginated list with page, limit, filter (all,read,unread), type, search. |
GET /counts | Counts by status and type for filters. |
GET /unread-count | Cached unread count. |
PATCH /:id/read | Mark a single notification read/unread. |
PATCH /mark-all-read | Mark all as read for the user. |
DELETE /:id | Delete a single notification. |
DELETE /delete-all | Delete all notifications for the user. |
GET /preferences | Fetch preferences (created on first read). |
PATCH /preferences | Update preferences (emailEnabled, pushEnabled, emailTypes, pushTypes). |
Admin Broadcast
- Endpoint:
POST /admin/notifications/send - Request schema:
AdminSendNotificationRequest(target.scope=user|users|all;notificationcontains type/title/content/metadata/deliveryOptions). - Batches sends (see
NOTIFICATION_SEND_BATCH_SIZE) and skips banned users; returns counts and failures for any unmatched identifiers. - Delivery options can override strategy order per send.
Frontend Integration
- SSE client:
packages/frontend-common/notification/notification-sse-client.tswrapsEventSourcewithwithCredentialsand per-event listeners. - React hook:
apps/frontend/src/hooks/api/useNotificationSSE.tsmanages connection, local store (bounded byNOTIFICATION_CONFIG.maxInMemoryNotifications), and unread sync. - Provider:
apps/frontend/src/providers/NotificationProvider.tsxwires SSE state into context, shows Sonner toasts for new items, and opens a modal for multi-action notifications. Mounted inRootAppProvider. - UI primitives:
NotificationBell,NotificationPanel,NotificationItem,NotificationPreferences(inapps/frontend/src/components/notifications/). These use query/mutation hooks for list, counts, mark read/delete, preference updates, and SSE live updates. - Allowed action domains:
NOTIFICATION_ACTION_ALLOWED_DOMAINSinpackages/shared/config/notification.tsguards button/link actions; update when adding new frontend domains.
useNotifications hook
- Exported from
apps/frontend/src/providers/NotificationProvider.tsx. - Returns
notifications,unreadCount,connectionStatus,errorfrom the SSE-powered store. - Must be used inside
NotificationProvider(throws otherwise). - Great for lightweight surfaces like badges, menus, or in-page lists without reimplementing SSE wiring.
Example: consuming notifications in the app
typescript
// Wrap the app (already done in RootAppProvider)
<NotificationProvider>
<AppLayout />
</NotificationProvider>
// Use inside components
const { notifications, unreadCount } = useNotifications();Delivery & Preference Rules
- Email is attempted only when the user is offline (
notification-sse-manager.isUserOnline) and the type is enabled in preferences (falls back toDEFAULT_EMAIL_TYPESwhen unset). - Strategy order can be overridden per send via
deliveryOptions.strategies; otherwise usesDELIVERY_STRATEGY_PRIORITY. - Retention defaults (
NOTIFICATION_RETENTION): max age 90 days, max 1,000 per user (housekeeping TBD).
Configuration & Environment
- Shared defaults live in
packages/shared/config/notification.ts:- SSE keep-alive/timeout intervals
- Max in-memory items, pagination defaults, toast duration
- Default delivery strategy order and per-type email/push defaults
- Allowed action domains derived from
FE_BASE_URL/FRONTEND_URL
- Environment variables involved:
REDIS_URL— unread count cache + pub/sub (required for best performance).FE_BASE_URL(orFRONTEND_URL) — used for action allowlist and email links.SMTP_*— required for email delivery; without SMTP the email strategy no-ops in dev.ENABLE_DISTRIBUTED_CHAT=true— also enables distributed notification fan-out.- CORS: ensure frontend/admin origins are in
CORS_ALLOWLISTED_ORIGINSfor SSE cookies.
Adding a New Notification Type
- Add to
NotificationTypeenum inpackages/shared/interfaces/notification.ts. - Decide default delivery (update
DEFAULT_EMAIL_TYPES/DEFAULT_PUSH_TYPESif needed). - Add icon mapping in
apps/frontend/src/lib/notification-icons.tsx. - Add i18n strings in
apps/frontend/locales/*/notifications.jsonand admin locales if used. - If action buttons are included, keep URLs within
NOTIFICATION_ACTION_ALLOWED_DOMAINS.
Testing & Verification
- API:
apps/api/test/notification-*.test.tscover service, router, and delivery flows. - Frontend:
apps/frontend/src/pages/notifications.test.tsxexercises SSE-driven UI, list, and preferences. - Run full suite before shipping:
bun run test --coverageandbun run lint.