Notification System
Architecture
User action (mutation succeeds)
→ showSuccessToast(title, message) show-notifications.tsx
→ notify() enqueues to Zustand store notification-store.ts
→ Sonner toast (if sonnerEnabled=true)
→ NotificationFlusher picks up queue notification-flusher.tsx
→ createNotification GraphQL mutation → backend persists to app_notification table
→ Bell badge polls userUnreadNotificationCount every 30s
→ User opens panel → immediate refetch + optional prune
Frontend Files
| File | Purpose |
|---|---|
src/stores/notification-store.ts | Zustand store: unread count, flush queue, settings |
src/lib/show-notifications.tsx | showSuccessToast / showFailedToast / showInfoToast / showWarningToast |
src/features/notifications/notification-flusher.tsx | Flushes queue to backend, polls unread count every 30s |
src/features/notifications/notification-bell.tsx | Bell icon with unread badge in header |
src/features/notifications/notification-panel.tsx | Sheet panel: list, mark-read, settings toggles |
src/features/notifications/hooks/use-notifications.ts | All GraphQL hooks |
src/components/layout/authenticated-layout.tsx | Mounts <NotificationFlusher /> and <NotificationBell /> |
Notification Message Format
The message field carries structured context using · as separator:
"Domain · Application Title · Visibility"
Examples:
"Application · My Tax App · Internal"— app-level internal comment"Document · My Tax App · External"— doc-slot external comment"My Tax App"— plain application title (create/update events)
parseContext() in notification-panel.tsx splits on · and returns { label, title, visibility }.
handleServerSuccess signature
handleServerSuccess(
request?,
response?,
translationKey?, // becomes notification title
invalidateQueryKeys?,
message? // becomes notification message (context/app-title)
)
Where Notifications Are Fired
Comment hooks (use-application-comments.ts)
useCreateApplicationComment(applicationId, applicationTitle?) — on success builds:
const contextLabel = input.applicationDocumentDefinitionId ? 'Document' : 'Application'
const visibility = input.type === 'INTERNAL' ? 'Internal' : 'External'
const message = applicationTitle
? `${contextLabel} · ${applicationTitle} · ${visibility}`
: `${contextLabel} · ${visibility}`
applicationTitle must be passed by the caller — not fetched inside the hook:
thread-detail.tsx→thread.applicationTitlemessages/index.tsx,comment-tab.tsx,doc-slot-tab.tsx→application?.titlefromuseApplicationContext
Application CRUD (use-applications.ts)
useCreateApplication→ message =variables.input.titleuseUpdateApplication→ message =data.title(from mapped response)
Applicant hooks (use-applications-applicants.ts)
All four hooks call useApplicationContext() internally and pass application?.title as message.
Document review (use-document-operations.ts)
useReviewApplicationDocument(applicationId, applicationTitle?) — optional second param from doc-slot-tab.tsx.
Adding a New Notification Source
- Call
showSuccessToast(title, appTitle)(or appropriate variant) — that's it for simple cases. - Inside a mutation
onSuccessusinghandleServerSuccess, pass the app title as the 5th argument. - For hooks inside
ApplicationProvider, calluseApplicationContext()to getapplication?.title. - The flusher handles persistence automatically — no extra wiring needed.