Notifications Configuration — Options & Health Checks
This page documents all configuration options, health checks, and OpenTelemetry instrumentation for the notification engine and its providers.
{ "Notifications": { "MaxParallelDeliveries": 8 }}| Property | Default | Description |
|---|---|---|
MaxParallelDeliveries | 8 | Max concurrent delivery messages (Wolverine queue parallelism) |
{ "Notifications": { "Email": { "Provider": "Smtp", "SenderAddress": "noreply@clinic.example.com", "SenderName": "Clinic Portal" } }}{ "Notifications": { "Smtp": { "Host": "smtp.example.com", "Port": 587, "UseSsl": true, "Username": "user", "Password": "vault:secret/data/smtp#password", "TimeoutSeconds": 30 } }}{ "Notifications": { "Brevo": { "ApiKey": "vault:secret/data/brevo#api-key", "DefaultSenderEmail": "noreply@clinic.example.com", "DefaultSenderName": "Clinic Portal", "DefaultSmsSenderId": "CLINIC", "BaseUrl": "https://api.brevo.com/v3", "TimeoutSeconds": 30 } }}SendGrid
Section titled “SendGrid”{ "Notifications": { "SendGrid": { "ApiKey": "vault:secret/data/sendgrid#api-key", "DefaultSenderEmail": "noreply@clinic.example.com", "DefaultSenderName": "Clinic Portal", "SandboxMode": false, "TimeoutSeconds": 30 } }}Twilio
Section titled “Twilio”{ "Notifications": { "Twilio": { "AccountSid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "AuthToken": "vault:secret/data/twilio#auth-token", "DefaultSmsFrom": "+15551234567", "DefaultWhatsAppFrom": "whatsapp:+14155238886", "MessagingServiceSid": null, "TimeoutSeconds": 30 } }}{ "Notifications": { "Sms": { "Provider": "Brevo", "SenderId": "CLINIC" } }}{ "Notifications": { "WhatsApp": { "Provider": "Twilio", "DefaultLanguage": "fr" } }}| Property | Default | Description |
|---|---|---|
Provider | — | Keyed Service name of the WhatsApp sender ("Twilio", "Brevo") |
DefaultLanguage | "fr" | Terminal fallback for the template language |
The template language is resolved per recipient through a cascade:
context.Culture → recipient.PreferredCulture → the
Granit.Localization.PreferredCulture setting (User → Tenant → Global via
ISettingProvider, soft dependency) → DefaultLanguage.
Mobile Push (FCM)
Section titled “Mobile Push (FCM)”{ "Notifications": { "MobilePush": { "Provider": "GoogleFcm" }, "GoogleFcm": { "ProjectId": "my-firebase-project", "ServiceAccountJson": "vault:secret/data/fcm#service-account", "BaseAddress": "https://fcm.googleapis.com/", "TimeoutSeconds": 30 } }}ServiceAccountJson is the Google service account key consumed to mint OAuth2
bearer tokens for the FCM HTTP v1 API (scope
https://www.googleapis.com/auth/firebase.messaging). Tokens are cached with a
5-minute refresh margin and minted single-flight — concurrent sends never
trigger parallel token requests; a 401 invalidates the cache and retries once.
Azure Communication Services (Email + SMS)
Section titled “Azure Communication Services (Email + SMS)”One package, two capabilities. Each capability is registered only when its configuration section exists — an SMS-only host never wires (or validates) the email sender, and vice versa.
{ "Notifications": { "AzureCommunicationServices": { "Email": { "ConnectionString": "endpoint=...;accesskey=...", "SenderAddress": "noreply@mydomain.com", "TimeoutSeconds": 120 }, "Sms": { "ConnectionString": "endpoint=...;accesskey=...", "FromPhoneNumber": "+15551234567", "TimeoutSeconds": 30 } } }}AWS SNS (SMS + Mobile Push)
Section titled “AWS SNS (SMS + Mobile Push)”One package, two capabilities, same SNS SDK. Each capability is registered only when its configuration section exists.
{ "Notifications": { "AwsSns": { "Sms": { "Region": "eu-west-1", "SenderId": "MYAPP", "SmsType": "Transactional", "TimeoutSeconds": 30 }, "MobilePush": { "Region": "eu-west-1", "PlatformApplicationArn": "arn:aws:sns:eu-west-1:123456789:app/GCM/my-app", "TimeoutSeconds": 30 } } }}Notifications:AwsSns:Sms:
| Property | Default | Description |
|---|---|---|
Region | — | AWS region (required) |
SenderId | null | Alphanumeric sender ID (1-11 chars, region-dependent) |
SmsType | "Transactional" | "Transactional" or "Promotional" |
OriginationNumber | null | E.164 originating number |
AccessKeyId | null | AWS access key (uses default credential chain when null) |
SecretAccessKey | null | AWS secret key (required if AccessKeyId set) |
Notifications:AwsSns:MobilePush:
| Property | Default | Description |
|---|---|---|
Region | — | AWS region (required) |
PlatformApplicationArn | — | SNS Platform Application ARN (required) |
AccessKeyId | null | AWS access key (uses default credential chain when null) |
SecretAccessKey | null | AWS secret key (required if AccessKeyId set) |
Azure Notification Hubs — Mobile Push
Section titled “Azure Notification Hubs — Mobile Push”{ "Notifications": { "AzureNotificationHubs": { "ConnectionString": "Endpoint=sb://...", "HubName": "my-notification-hub", "TimeoutSeconds": 30 } }}SignalR
Section titled “SignalR”{ "Notifications": { "SignalR": { "RedisConnectionString": "redis:6379" } }}Web Push (VAPID)
Section titled “Web Push (VAPID)”{ "Notifications": { "WebPush": { "VapidSubject": "mailto:admin@clinic.example.com", "VapidPublicKey": "BFx...", "VapidPrivateKey": "vault:secret/data/webpush#private-key" } }}{ "Notifications": { "Sse": { "HeartbeatIntervalSeconds": 30, "MaxConnectionsPerUser": 10, "MaxBufferSize": 100 } }}| Option | Default | Description |
|---|---|---|
HeartbeatIntervalSeconds | 30 | Keep-alive interval through proxies |
MaxConnectionsPerUser | 10 | Concurrent SSE connections per user (covers multiple tabs/devices) |
MaxBufferSize | 100 | Messages buffered per connection before oldest are dropped |
SSE Redis backplane (multi-replica)
Section titled “SSE Redis backplane (multi-replica)”SSE connections are per-pod: without a backplane, a notification published on
pod A never reaches a user connected to pod B. Add
Granit.Notifications.Sse.StackExchangeRedis to fan deliveries out over Redis
pub/sub (reuses the shared IConnectionMultiplexer from
Granit.Caching.StackExchangeRedis):
builder.Services.AddGranitNotificationsSseRedisBackplane();{ "Notifications": { "Sse": { "StackExchangeRedis": { "ChannelName": "granit-notifications-sse" } } }}| Option | Default | Description |
|---|---|---|
ChannelName | "granit-notifications-sse" | Redis pub/sub channel carrying the SSE envelopes |
Malformed backplane messages are logged, counted
(granit.notifications.sse.backplane.message_dropped), and skipped — a poison
message never kills the subscriber loop. Outside the Development environment,
running the SSE channel without a backplane logs a one-shot warning at the
first delivery, because multi-replica delivery would silently be partial.
{ "Notifications": { "Zulip": { "DefaultStream": "alerts", "DefaultTopic": "system", "Bot": { "BaseUrl": "https://zulip.example.com", "BotEmail": "notification-bot@zulip.example.com", "ApiKey": "vault:secret/data/zulip#api-key", "TimeoutSeconds": 30 } } }}Health checks
Section titled “Health checks”Notification channel providers register opt-in health checks:
builder.Services.AddHealthChecks() .AddGranitSmtpHealthCheck() .AddGranitAwsSesHealthCheck() .AddGranitBrevoHealthCheck() .AddGranitAcsEmailHealthCheck() .AddGranitAcsSmsHealthCheck() .AddGranitAzureNotificationHubsHealthCheck() .AddGranitAwsSnsSmsHealthCheck() .AddGranitAwsSnsMobilePushHealthCheck() .AddGranitGoogleFcmHealthCheck() .AddGranitScalewayEmailHealthCheck() .AddGranitSendGridHealthCheck() .AddGranitTwilioHealthCheck() .AddGranitZulipHealthCheck();| Provider | Extension | Probe | Tags |
|---|---|---|---|
| SMTP | AddGranitSmtpHealthCheck() | EHLO handshake via MailKit | readiness |
| SES | AddGranitAwsSesHealthCheck() | GetAccount() — Degraded if sending paused | readiness, startup |
| Brevo | AddGranitBrevoHealthCheck() | GET /account | readiness |
| ACS Email | AddGranitAcsEmailHealthCheck() | Non-mutating config/credential validation (never sends) | readiness |
| ACS SMS | AddGranitAcsSmsHealthCheck() | Non-mutating config/credential validation (never sends) | readiness |
| Azure Notification Hubs | AddGranitAzureNotificationHubsHealthCheck() | Hub description retrieval | readiness |
| SNS SMS | AddGranitAwsSnsSmsHealthCheck() | SNS API connectivity check | readiness, startup |
| SNS Mobile Push | AddGranitAwsSnsMobilePushHealthCheck() | SNS Platform Application check | readiness, startup |
| Google FCM | AddGranitGoogleFcmHealthCheck() | Config validation (ProjectId + parseable service account) | readiness, startup |
| Scaleway TEM | AddGranitScalewayEmailHealthCheck() | GET /emails?page_size=1 | readiness, startup |
| SendGrid | AddGranitSendGridHealthCheck() | GET /scopes | readiness, startup |
| Twilio | AddGranitTwilioHealthCheck() | GET /Accounts/{sid}.json | readiness, startup |
| Zulip | AddGranitZulipHealthCheck() | GET /api/v1/users/me (Bot auth) | readiness |
All checks sanitize error messages — credentials, hostnames, and API keys are never
exposed in the health check response. Every check enforces a 10-second defensive timeout
via .WaitAsync() to prevent blocking Kubernetes probe cycles.
OpenTelemetry
Section titled “OpenTelemetry”All fan-out and delivery operations are traced via ActivitySource:
| Activity name | Description |
|---|---|
notifications.fanout | Fan-out of a NotificationTrigger into delivery commands |
notifications.deliver | Delivery of a single DeliverNotificationCommand via a channel |
Tags: notifications.type, notifications.channel, notifications.delivery_id,
notifications.notification_id, notifications.recipient_count,
notifications.delivery_count, notifications.success.
See also
Section titled “See also”- Overview — package structure and setup
- Email channel — provider-specific email options
- SMS, push & real-time — provider-specific SMS/push/realtime options
- Wolverine integration — durable dispatch options
- Set up notifications guide — step-by-step provider wiring
- Observability — common metrics, tracing, and log patterns