Skip to content

Notifications

Overview

Vitara uses a dual-channel notification system: an in-app notification inbox (the bell icon in the navigation bar) and browser Web Push notifications. Every notification is always persisted to the in-app inbox first; a push is sent additionally if the user has a registered subscription and has not opted out of that notification type. Users can configure which types of push notifications they receive from Profile → Notifications. Administrators can override a user's preferences for health-alert types that are designated as admin-controlled.


Role Access

Feature All Roles Admin/SuperAdmin Only
View in-app notifications
Mark notifications read
Configure own non-health preferences
Configure health-alert preferences ✓ (via admin panel)
Manage any user's preferences
Send test push ✓ (self, any type) Required in production

Notification Types

ID Name Admin-Controlled
1 General No
2 IncidentSeverity Yes
3 AssessmentSeverity Yes
4 CarerRemoved No
5 CareRecipientRemoved No
6 BgTimerReminder Yes
7 DelinkApproved No
8 DelinkDenied No
9 BloodPressureAlert Yes
10 FeatureBugReportResolved No
11 SupplyRunningLow No
12 BpIncidentSeverity Yes

Admin-controlled types (IDs 2, 3, 6, 9, 12) can only be toggled by Administrators and SuperAdmins, not by the users themselves.


Backend

Controller: NotificationController/api/notification

Method Route Auth Description
GET /api/notification Authorized Get all notifications + unread count for current user
PUT /api/notification/{id}/read Authorized Mark one notification as read
PUT /api/notification/read-all Authorized Mark all notifications as read
POST /api/notification/ketone-alert SupportOrHigher Manually trigger ketone alert for a care recipient

Controller: NotificationPreferenceController

Method Route Auth Policy Description
GET /api/notification-preferences Authorized Get own preferences (all 12 types)
PUT /api/notification-preferences Authorized Update own non-admin-controlled preferences
GET /api/admin/notification-preferences/{userId} AdminOrHigher Get any user's preferences
PUT /api/admin/notification-preferences/{userId} AdminOrHigher Update any type for any user

Controller: PushController/api/push

Method Route Auth Policy Description
GET /api/push/vapid-public-key Anonymous Returns VAPID public key
POST /api/push/subscribe Authorized Register or refresh a browser push subscription
DELETE /api/push/unsubscribe Authorized Remove a push subscription by endpoint
POST /api/push/test Authorized (Admin in prod) Send a test push notification to self
POST /api/push/bg-timer/schedule AllRoles Schedule a BGL recheck timer reminder
DELETE /api/push/bg-timer/cancel AllRoles Cancel a pending BG timer reminder

POST /api/push/subscribe — Upsert Subscription

endpoint     string    Web Push endpoint URL
p256dh       string    Public key (base64)
auth         string    Auth secret (base64)
userAgent    string?   Browser user agent
Finds existing PushSubscription by endpoint or creates a new one. Updates the Auth and P256dh keys if they changed.

POST /api/push/bg-timer/schedule

careRecipientId   int    Target care recipient
delayMinutes      int?   Default 120 min, max 480 min
Cancels any existing pending BgTimerReminder for the same (userId, careRecipientId) pair, then inserts a new one with ScheduledAt = now + delayMinutes.

Service: NotificationService

File: server/src/Vitara.Api/Services/NotificationService.cs

Dependencies: ApplicationDbContext

Operations: - CreateAsync(userId, message, type, deepLinkUrl?, relatedEntityId?) — persists a Notification row. - GetForUserAsync(userId) — returns NotificationSummaryDto with a list of notifications + total unread count. - MarkReadAsync(id, userId) — sets IsRead = true for a single notification (validates ownership). - MarkAllReadAsync(userId) — bulk update via ExecuteUpdateAsync.

Service: PushNotificationSender

File: server/src/Vitara.Api/Services/PushNotificationSender.cs

Dependencies: ApplicationDbContext, NotificationService, VAPID settings, HttpClient

SendToUserAsync(userId, payload) — full pipeline:

  1. Always creates in-app notification via NotificationService.CreateAsync() first.
  2. Preference check: Queries UserNotificationPreferences for this user + notification type. If IsEnabled = false, skips push (in-app record still created).
  3. Load subscriptions: Queries all PushSubscription rows for the user.
  4. Dispatch: Sends VAPID-signed HTTP POST to each subscription endpoint using Lib.Net.Http.WebPush. All dispatches run in parallel (Task.WhenAll).
  5. Pruning: Any subscription that returns HTTP 410 (Gone) or 404 is automatically deleted from the database.

SendToUsersAsync(userIds[], payload) — calls SendToUserAsync for each ID.

Service: NotificationPreferenceService

File: server/src/Vitara.Api/Services/NotificationPreferenceService.cs

GetPreferencesAsync(userId) - Loads all 12 NotificationTypeEntity rows. - Joins with UserNotificationPreference rows for the user. - For types with no stored preference row, defaults to IsEnabled = true. - Returns a complete list of 12 NotificationPreferenceDto items.

UpdatePreferencesAsync(userId, items, isAdmin) - For each item in the request: - If isAdmin = false and the type's IsAdminControlled = true, skips the item silently. - Otherwise, upserts the UserNotificationPreference row for (userId, typeId).

Model: Notification

Field Type Notes
Id int PK
UserId int FK → User
Message string Notification text
IsRead bool
CreatedAt DateTime
Type NotificationType Enum 1–12
DeepLinkUrl string? Angular route to navigate on click
RelatedEntityId int? ID of the related entity (incident, etc.)
SentViaPush bool Whether a push was dispatched

Model: PushSubscription

Field Type Notes
Id int PK
UserId int FK → User
Endpoint string Web Push endpoint URL
P256dh string Client public key
Auth string Auth secret
UserAgent string?
CreatedAt DateTime

Model: UserNotificationPreference

Field Type Notes
UserId int PK (composite)
NotificationTypeId int PK (composite) FK → NotificationTypeEntity
IsEnabled bool

Frontend

In-App Notification Bell

File: client/src/app/app.component.ts

  • Bell icon in the top navigation bar with a badge showing the unread count.
  • On icon click, calls NotificationService.getNotifications() (GET /notification) to load the full inbox.
  • Dropdown panel lists notifications ordered by createdAt descending.
  • Clicking a notification: marks it read (PUT /notification/{id}/read), then navigates to DeepLinkUrl if present.
  • "Mark All Read" button: PUT /notification/read-all.

Push Subscription Management

File: client/src/app/core/services/push-notification.service.ts

subscribeToServer() — called on every login: 1. Checks Notification.permission; if not granted, requests permission. 2. Fetches VAPID public key from GET /push/vapid-public-key. 3. Calls navigator.serviceWorker.readyregistration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: vapidKey }). 4. Posts subscription to POST /push/subscribe with endpoint, p256dh, auth.

unsubscribeFromServer() 1. Gets current subscription from pushManager.getSubscription(). 2. Calls subscription.unsubscribe() (browser side). 3. Calls DELETE /push/unsubscribe with the endpoint.

sendTestPush(payload)POST /push/test.

Notification Preferences Component

File: client/src/app/features/profile/components/notification-preferences.component.ts Route: /profile/notifications

  • Loads GET /notification-preferences on init.
  • Health Alerts section: Types with isAdminControlled = true shown as read-only labels with their current state (enabled/disabled).
  • My Preferences section: Types with isAdminControlled = false shown as Material slide toggles.
  • Save button: Calls PUT /notification-preferences with the array of changed preferences.
  • Success/error snackbar on save.

Admin: Per-User Notification Preferences

File: client/src/app/features/management/components/user-management.component.ts

  • Bell icon button per user row in the User Management table.
  • Opens a modal panel loaded with GET /admin/notification-preferences/{userId}.
  • Displays all 12 notification types with individual toggle switches.
  • Save calls PUT /admin/notification-preferences/{userId}.

Notification Seeding on Registration

When a new user registers (AuthService.SignupAsync), all 12 UserNotificationPreference rows are inserted with IsEnabled = true to ensure the user receives all notifications by default until they explicitly opt out.


Service Worker Push Handling

When the browser receives a push event, the service worker (client/src/custom-sw.js) processes it: 1. Parses the push data payload (JSON: { title, body, deepLinkUrl?, type }). 2. Calls self.registration.showNotification(title, { body, data: { deepLinkUrl } }). 3. On notificationclick event: focuses an existing Vitara window if open, or opens a new one, navigating to deepLinkUrl if provided.


End-to-End Push Flow

API                         PushNotificationSender           Browser
  |                         |                                  |
  | Incident severity High  |                                  |
  |-- SendToUserAsync(carerId, payload)                        |
  |                         |-- create in-app notification     |
  |                         |-- check preference: enabled?     |
  |                         |-- load subscriptions             |
  |                         |-- VAPID-signed POST to endpoint->|
  |                         |                                  |-- show notification
  |                         |                                  | (even if tab closed)
  |                         | 410? → delete subscription       |
  |                         |                                  |
  | Carer clicks notification|                                 |
  |                         |<-- notificationclick event ------|
  |                         |-- navigate to deepLinkUrl        |
  |                         |-- PUT /notification/{id}/read -->|