Notifications
Genie has a store-and-forward notification system: callers queue a notification, background worker services pick it up and deliver it over the appropriate channel, and in-app messages arrive in real time over SignalR (with a browser push fallback for offline users).
The pipeline
Section titled “The pipeline”┌─────────────┐ ┌───────────────────┐ ┌─────────────────┐ ┌────────────────────┐│ queue │ │ NotificationStore │ │ worker service │ │ channel delivery ││ API/service │ ─▶ │ persisted in DB │ ─▶ │ polls + retries │ ─▶ │ App·Email·SMS·push │└─────────────┘ └───────────────────┘ └─────────────────┘ └────────────────────┘Every notification is persisted first, then delivered by a worker on its next cycle. Delivery is retried (up to a max attempt count) and marked delivered on success, so nothing is lost if a channel is briefly unavailable.
Channels & workers
Section titled “Channels & workers”Under Features/Notifications/Workers/, each channel has a MonitoredBackgroundService that drains the
store in batches:
| Worker | Channel | Status |
|---|---|---|
AppNotificationWorkerService |
In-app (SignalR) + browser push | Implemented |
EmailNotificationWorkerService |
Implemented — SMTP delivery (MailKit), configured via SmtpSettings |
|
SmsNotificationWorkerService |
SMS | Batching/retry wired; the provider send is a stub (Twilio/Vonage integration pending) |
The app worker delivers each in-app notification by pushing a ReceiveNotification event to the
recipient over SignalR and sending a Web Push message to every registered browser subscription;
delivery counts as successful if either path works. Push uses VAPID keys from configuration
(Notifications:Vapid:PublicKey / PrivateKey / Subject); when a subscription returns 410 Gone or
404, it is marked inactive automatically.
Real-time delivery (SignalR)
Section titled “Real-time delivery (SignalR)”The GenieNotificationHub (mapped via MapGenieHubs()) delivers per-user pushes. It requires
authentication so the hub can resolve UserIdentifier — Clients.User(userId) only reaches a
connection whose identity matches, so an anonymous connection silently drops per-user messages. The UI’s
SignalR hook subscribes to ReceiveNotification and updates the notification bell live.
The current user’s own notifications — /api/v1/notification
Section titled “The current user’s own notifications — /api/v1/notification”| Method | Route | Purpose |
|---|---|---|
GET |
/unread?limit= |
list unread notifications |
GET |
/read?limit= |
list read notifications |
POST |
/{id}/read |
mark one as read (recipient only) |
POST |
/mark-all-read |
mark all unread as read |
Sending notifications (external integration) — /api/v1/notifications
Section titled “Sending notifications (external integration) — /api/v1/notifications”A JWT-bearer, role-gated surface (Authorize(Roles = "NotifUser")) for external callers to queue a
notification. Each returns a NotificationResponse with the new notification id.
| Method | Route | Body |
|---|---|---|
POST |
/app |
{ UserName, Subject, Body, ScheduledAt? } |
POST |
/email |
{ Email, Subject, Body, CC?, BCC?, ScheduledAt? } |
POST |
/sms |
{ PhoneNumber, Subject, Body, ScheduledAt? } |
POST |
/whatsapp |
{ PhoneNumber, Subject, Body, ScheduledAt? } |
ScheduledAt lets a caller defer delivery; the worker only picks up notifications whose scheduled time
has passed.
Browser push subscriptions — /api/v1/push-subscription
Section titled “Browser push subscriptions — /api/v1/push-subscription”| Method | Route | Purpose |
|---|---|---|
GET |
/vapid-public-key |
the VAPID public key the browser needs to subscribe |
POST |
/register |
register a PushSubscription (endpoint + keys.p256dh/auth) |
POST |
/unregister |
remove a subscription by endpoint |
GET |
/my-subscriptions |
list the current user’s active subscriptions |
Monitoring
Section titled “Monitoring”The notification workers are MonitoredBackgroundServices, so they appear in the hosted-services
dashboard with live metrics and pause/resume controls — see
Background jobs & hosted services.