Notification Architecture
Persisted notifications, realtime refresh events, completion email, and failure behavior in the web app.
Notification Architecture
The Web App has two separate notification mechanisms:
- In-app notifications are PostgreSQL
Notificationrecords. Pusher events tell an open browser to refetch those records. - Email notifications are sent through Nodemailer for selected dataset-access, analysis, and report events.
The Model Server's Redis notifications queue is a third, separate concept: it delivers worker-status callbacks to Web App API routes. Those callbacks may cause email, but they do not create Web App Notification rows automatically.
In-App Notification Flow
Current producers are dataset-access workflows:
| Event | Durable audience | Realtime event | Additional delivery |
|---|---|---|---|
| User requests dataset access | Role admin | evt::admin | None |
| Dataset request is approved | Currently the acting user's ID | evt::user.{actingUserId} | Approval email to the requester |
The second row reflects current implementation, not the desired recipient model: the approval route emails the requester but creates the in-app notification for the acting user. Correcting the recipient requires an application change and a regression test.
The browser subscribes to the notifications Pusher channel and binds only the event name relevant to the current user or administrator. The event is an invalidation signal; the database query, rather than the event payload, supplies the notification list.
Email and Callback Flow
| Trigger | Current failure behavior | |
|---|---|---|
| Analysis completion | A Marsad, Tanbih, or OpenRouter callback derives or receives completed. | The mail helper logs delivery errors and does not rethrow them. The callback can return success even when email was not delivered. There is no durable email job or automatic retry. |
| Report completion | A valid report callback has status completed. | A mail failure returns HTTP 500 with status email_failed. The report remains generated, but successful notification is not recorded separately. A later callback retry can attempt the email again. |
| Dataset access granted | An authenticated request changes a dataset request to approved. | The database status is updated before email is sent. If email or the following in-app notification fails, the route returns 500 even though the approval may already be committed; retry can repeat delivery work. |
Analysis failure statuses currently do not send the failure form of the analysis email, even though the mail template helper supports it.
Current Failure and Security Boundaries
The following behaviors are important when debugging or changing notifications:
- Realtime is best-effort.
createNotificationcurrently awaits the Pusher publish before awaiting the Prisma insert. A browser can refetch before the row is committed. If Pusher fails, the pending insert is not awaited and the notification is not durable; if insertion then fails after a successful publish, the browser receives an event with no stored notification. - There is no outbox or delivery ledger. Database state, Pusher delivery, and email delivery are not transactionally coordinated. The system cannot reliably prove that a recipient received an event or email.
- The Pusher notification channel is currently public. Event names contain user IDs, but event-name filtering is not an authorization boundary. Do not include private content or credentials in realtime payloads. Moving notifications to authenticated private or presence channels requires authorization in
/api/pusher/auth. - Admin notification reads are not role-gated. The admin-list API currently verifies that a user is signed in but does not verify the admin role.
- Mark-as-read is not ownership-checked. The read API updates a notification by ID without verifying that the current user belongs to its user or role audience.
- Role notifications have one shared read field. Because a notification has one nullable
readById, one administrator marking a role notification read makes that same row appear read to every administrator. Per-user read state requires a separate recipient/read-receipt relation. - Callbacks can be duplicated. Model Server callback delivery and manual recovery may repeat a terminal update. Email and notification side effects therefore need an idempotency record before retries can be considered safe.
Operational Checks
When a user reports a missing or duplicate notification, correlate the application entity ID, callback response, notification record ID, Pusher event, and mail-provider result. Check in this order:
- Confirm that the originating dataset request or processing job reached the expected state.
- For worker-driven completion, confirm delivery through the Model Server
notificationsqueue and the Web App callback response. - Query the Web App
Notificationrow for in-app messages; a Pusher event alone is not durable evidence. - Check that the notification audience and event name identify the intended recipient.
- Check Pusher or email-provider errors without logging message contents, addresses, credentials, callback bodies, or uploaded data.
- Before replaying a callback or mutation, determine whether the database change already committed and whether replay can send a duplicate email.
See Integration Boundaries for callback ownership and Cross-Repository Contracts for the canonical callback routes and payloads.