Skip to main content

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

FilePurpose
src/stores/notification-store.tsZustand store: unread count, flush queue, settings
src/lib/show-notifications.tsxshowSuccessToast / showFailedToast / showInfoToast / showWarningToast
src/features/notifications/notification-flusher.tsxFlushes queue to backend, polls unread count every 30s
src/features/notifications/notification-bell.tsxBell icon with unread badge in header
src/features/notifications/notification-panel.tsxSheet panel: list, mark-read, settings toggles
src/features/notifications/hooks/use-notifications.tsAll GraphQL hooks
src/components/layout/authenticated-layout.tsxMounts <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.tsxthread.applicationTitle
  • messages/index.tsx, comment-tab.tsx, doc-slot-tab.tsxapplication?.title from useApplicationContext

Application CRUD (use-applications.ts)

  • useCreateApplication → message = variables.input.title
  • useUpdateApplication → 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

  1. Call showSuccessToast(title, appTitle) (or appropriate variant) — that's it for simple cases.
  2. Inside a mutation onSuccess using handleServerSuccess, pass the app title as the 5th argument.
  3. For hooks inside ApplicationProvider, call useApplicationContext() to get application?.title.
  4. The flusher handles persistence automatically — no extra wiring needed.