Skip to content

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).

┌─────────────┐ ┌───────────────────┐ ┌─────────────────┐ ┌────────────────────┐
│ queue │ │ NotificationStore │ │ worker service │ │ channel delivery │
│ API/service │ ─▶ │ persisted in DB │ ─▶ │ polls + retries │ ─▶ │ App·Email·SMS·push │
└─────────────┘ └───────────────────┘ └─────────────────┘ └────────────────────┘
Prefer a picture?

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.

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 Email 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.

The GenieNotificationHub (mapped via MapGenieHubs()) delivers per-user pushes. It requires authentication so the hub can resolve UserIdentifierClients.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

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.