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
PushSubscription by endpoint or creates a new one. Updates the Auth and P256dh keys if they changed.
POST /api/push/bg-timer/schedule¶
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:
- Always creates in-app notification via
NotificationService.CreateAsync()first. - Preference check: Queries
UserNotificationPreferencesfor this user + notification type. IfIsEnabled = false, skips push (in-app record still created). - Load subscriptions: Queries all
PushSubscriptionrows for the user. - Dispatch: Sends VAPID-signed HTTP POST to each subscription endpoint using
Lib.Net.Http.WebPush. All dispatches run in parallel (Task.WhenAll). - 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
createdAtdescending. - Clicking a notification: marks it read (
PUT /notification/{id}/read), then navigates toDeepLinkUrlif 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.ready → registration.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-preferenceson init. - Health Alerts section: Types with
isAdminControlled = trueshown as read-only labels with their current state (enabled/disabled). - My Preferences section: Types with
isAdminControlled = falseshown as Material slide toggles. - Save button: Calls
PUT /notification-preferenceswith 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 -->|