Set Up Notifications
Granit.Notifications is a fan-out notification engine that delivers messages across
multiple channels from a single PublishAsync() call. This guide walks you through
installing the engine, registering channels, defining notification types, and
publishing notifications to users.
Prerequisites
Section titled “Prerequisites”- A working Granit application with
Granit.Wolverineconfigured - PostgreSQL database (for durable stores)
- Redis instance (if using SignalR backplane or caching)
1. Install the packages
Section titled “1. Install the packages”Add the core package and the channels you need:
dotnet add package Granit.Notificationsdotnet add package Granit.Notifications.EntityFrameworkCoredotnet add package Granit.Notifications.EndpointsThen add one or more channel packages depending on your requirements:
dotnet add package Granit.Notifications.Emaildotnet add package Granit.Notifications.Email.Smtpdotnet add package Granit.Notifications.Emaildotnet add package Granit.Notifications.Smsdotnet add package Granit.Notifications.WhatsAppdotnet add package Granit.Notifications.Brevodotnet add package Granit.Notifications.SignalRdotnet add package Granit.Notifications.WebPushdotnet add package Granit.Notifications.Emaildotnet add package Granit.Notifications.Email.AzureCommunicationServicesdotnet add package Granit.Notifications.Smsdotnet add package Granit.Notifications.Sms.AzureCommunicationServicesdotnet add package Granit.Notifications.MobilePushdotnet add package Granit.Notifications.MobilePush.AzureNotificationHubs2. Register the module
Section titled “2. Register the module”Declare the module dependency in your application module:
using Granit.Core.Modularity;using Granit.Notifications.EntityFrameworkCore;
[DependsOn(typeof(GranitNotificationsEntityFrameworkCoreModule))]public sealed class MyAppModule : GranitModule { }Register the EF Core store with your database provider:
builder.AddGranitNotificationsEntityFrameworkCore( opts => opts.UseNpgsql(connectionString));3. Register channels
Section titled “3. Register channels”Add each channel in Program.cs. Only register the channels your application needs —
unregistered channels are skipped with a log warning (graceful degradation).
// Real-time: choose ONE of SSE or SignalR (mutually exclusive)builder.Services.AddGranitNotificationsSignalR("redis:6379");// OR: builder.Services.AddGranitNotificationsSse();
// Email via MailKit SMTPbuilder.Services.AddGranitNotificationsEmail();builder.Services.AddGranitNotificationsEmailSmtp();
// SMSbuilder.Services.AddGranitNotificationsSms();
// Web Push (W3C VAPID -- no dependency on FCM or APNs)builder.Services.AddGranitNotificationsPush();4. Map endpoints
Section titled “4. Map endpoints”app.MapGranitNotificationEndpoints();This registers the inbox, preferences, subscriptions, and entity follower endpoints
under the /notifications prefix.
5. Configure channels
Section titled “5. Configure channels”Add the configuration for each registered channel in appsettings.json:
{ "Notifications": { "MaxParallelDeliveries": 8, "Email": { "Provider": "Smtp", "SenderAddress": "noreply@example.com", "SenderName": "My Application" }, "Smtp": { "Host": "smtp.example.com", "Port": 587, "UseSsl": true, "Username": "api-user", "Password": "vault://secret/smtp-password" }, "Push": { "VapidSubject": "mailto:admin@example.com", "VapidPublicKey": "BBase64UrlSafe...", "VapidPrivateKey": "vault://secret/vapid-private-key" }, "SignalR": { "RedisConnectionString": "redis:6379" } }}6. Define notification types
Section titled “6. Define notification types”Each notification type is declared as a class deriving from NotificationType<TData>:
public sealed class OrderShippedNotification : NotificationType<OrderShippedData>{ public static readonly OrderShippedNotification Instance = new();
public override string Name => "Orders.Shipped"; public override NotificationSeverity DefaultSeverity => NotificationSeverity.Info; public override IReadOnlyList<string> DefaultChannels => [NotificationChannels.InApp, NotificationChannels.Email, NotificationChannels.Push];}
public sealed record OrderShippedData{ public required string OrderId { get; init; } public required string TrackingNumber { get; init; }}Register notification definitions via a provider:
public sealed class OrderNotificationDefinitionProvider : INotificationDefinitionProvider{ public void Define(INotificationDefinitionContext context) { context.Add(new NotificationDefinition("Orders.Shipped") { DisplayName = "Order shipped", Description = "Sent when an order is shipped to the customer.", GroupName = "Orders", DefaultChannels = [NotificationChannels.InApp, NotificationChannels.Email], AllowUserOptOut = true, }); }}
// In ConfigureServices:services.AddNotificationDefinitions<OrderNotificationDefinitionProvider>();7. Publish notifications
Section titled “7. Publish notifications”Inject INotificationPublisher and choose one of three publishing strategies:
public sealed class OrderService(INotificationPublisher notifications){ public async Task ShipOrderAsync(Order order, CancellationToken cancellationToken) { // ... business logic ...
await notifications.PublishAsync( OrderShippedNotification.Instance, new OrderShippedData { OrderId = order.Id.ToString(), TrackingNumber = order.TrackingNumber, }, recipientUserIds: [order.CustomerId], cancellationToken); }}await notifications.PublishToSubscribersAsync( SystemMaintenanceNotification.Instance, new SystemMaintenanceData { ScheduledAt = maintenanceDate }, cancellationToken);await notifications.PublishToEntityFollowersAsync( PatientStatusChangedNotification.Instance, new PatientStatusChangedData { NewStatus = "Active" }, relatedEntity: new EntityReference("Patient", patient.Id.ToString()), cancellationToken);8. Entity tracking (Odoo-style followers)
Section titled “8. Entity tracking (Odoo-style followers)”For automatic notifications when tracked properties change, implement ITrackedEntity
on your EF Core entities:
public sealed class Patient : Entity, ITrackedEntity{ public static string EntityTypeName => "Patient"; public string GetEntityId() => Id.ToString();
public string Status { get; set; } = string.Empty;
public static IReadOnlyDictionary<string, TrackedPropertyConfig> TrackedProperties => new Dictionary<string, TrackedPropertyConfig> { ["Status"] = new() { NotificationTypeName = "Patient.StatusChanged", Severity = NotificationSeverity.Warning, }, };}The EntityTrackingInterceptor (provided by Granit.Notifications.EntityFrameworkCore)
detects changes during SaveChangesAsync and automatically publishes notifications to
entity followers.
Users follow or unfollow entities via the REST API:
POST /notifications/entity/{entityType}/{entityId}/followDELETE /notifications/entity/{entityType}/{entityId}/follow
Delivery tracking and retry
Section titled “Delivery tracking and retry”Every delivery attempt is recorded in an immutable NotificationDeliveryAttempt table
(ISO 27001 audit trail). Failed deliveries are retried with exponential backoff:
| Attempt | Delay | Elapsed |
|---|---|---|
| 1 | 10 seconds | 10 s |
| 2 | 1 minute | ~1 min 10 s |
| 3 | 5 minutes | ~6 min 10 s |
| 4 | 30 minutes | ~36 min 10 s |
| 5 | 2 hours | ~2 h 36 min |
After 5 attempts, the message moves to the Wolverine Dead Letter Queue.
REST API overview
Section titled “REST API overview”| Method | Route | Description |
|---|---|---|
GET | /notifications | Paginated inbox |
GET | /notifications/unread/count | Unread count |
POST | /notifications/{id}/read | Mark as read |
POST | /notifications/read-all | Mark all as read |
GET | /notifications/preferences | User preferences |
PUT | /notifications/preferences | Update a preference |
GET | /notifications/types | Registered notification types |
POST | /notifications/subscriptions/{typeName} | Subscribe to a type |
DELETE | /notifications/subscriptions/{typeName} | Unsubscribe |
Next steps
Section titled “Next steps”- Implement webhooks for outbound system-to-system event delivery
- Add background jobs for scheduled tasks
- Granit.Notifications reference for the full API surface and configuration options