System overview
The SharePoint Advanced Notification Manager is the designated replacement for Microsoft's retired native SharePoint Alert feature. It provides a richer, multi-channel notification service — users manage alert subscriptions through an SPFx extension embedded in a SharePoint list toolbar. Alert data is persisted in Azure SQL via an Azure Functions backend.
When list items change, SharePoint dispatches a webhook to the backend, which queues the event as a TriggerEvent and dispatches notifications on a 5-minute schedule via Email (Microsoft Graph), SMS, or Microsoft Teams (Bot Connector).
Components
The system spans four infrastructure layers. Each layer's responsibilities are strictly separated — the frontend never writes to SQL directly, and the backend never issues UI-layer calls.
SharePoint Online
Hosts the list and surfaces the SPFx ListViewCommandSet extension in the toolbar. The NotifyConfigurationWebPart provides tenant-admin UI. SharePoint triggers webhooks on item changes.
Azure AD / Entra
Hosts the app registration that issues Bearer tokens to the SPFx extension. The backend validates these tokens using OIDC metadata, and also acquires OBO and client credential tokens for Graph / SharePoint access.
Azure Functions backend
Four internal services running as Azure Functions: HTTP Endpoints (the REST API), Webhook Receiver (change ingestion, fire-and-forget), Scheduler (5-minute timer trigger), and Change Manager (CSOM diff + notification dispatch). SQL persistence via AlertInfo, TriggerEvent, WebHooks, and Log tables.
Notification channels
Email delivered via Microsoft Graph sendMail. SMS via HTTP to a configured provider. Microsoft Teams via the Bot Connector with Adaptive Cards. Channel chosen per-alert based on DeliveryMethod.
Authentication flow
The SPFx extension uses SharePoint's built-in AadHttpClient to acquire Azure AD Bearer tokens transparently — no manual OAuth flow in frontend code. Every backend HTTP function independently validates the token; no API gateway performs centralised validation.
AadHttpClient, which uses SharePoint's token brokering to request a JWT for api://<client-id> from Azure AD. No redirect or popup required.
Authorization: Bearer <token> carries the JWT. SPTenantID: <tenant-guid> identifies the calling tenant so the backend can resolve the correct OIDC metadata endpoint.
TokenValidatorCache to retrieve and cache the tenant's OpenID Connect metadata and signing keys, avoiding repeated round-trips on subsequent requests.
SPTenantID header is mandatory. Without it the backend cannot determine which tenant's OIDC metadata to load, and will reject the request even if the JWT itself is valid.Multi-tenant architecture
A single backend deployment serves multiple SharePoint tenants. Isolation is enforced at every layer — per-request, in the database, and in the scheduler.
TenantConfiguration schema
Isolation mechanisms
- Every HTTP request carries the
SPTenantIDheader, which the backend uses to scope all database reads and writes to that tenant only. TokenValidatorCachemaintains separate per-tenant OIDC validation parameters (signing keys, issuer URI).SchedulerNotificationiterates all registered tenants independently, instantiating a separateChangeManagerand credential set per tenant per run.- All SQL queries include a
TenantIDpredicate — there are no cross-tenant queries.
Onboarding a new tenant
.sppkg bundle to the tenant's SharePoint App Catalog and approve the API permissions so the extension can acquire tokens.
NotifyConfigurationWebPart to POST /api/configmngr/create with the tenant credentials. The backend begins processing that tenant on the next scheduler cycle.
Alert lifecycle
An alert goes through four distinct phases — from creation through change detection, scheduled notification delivery, and log viewing. Each phase is handled by a different backend component.
Alert creation
- User clicks "My Notifications" in the SharePoint list toolbar.
- SPFx loads
AlertFormPaneland callsGET /api/alerts4list/{listId}to pre-populate existing alerts (scoped to the user). - User fills in the alert form and clicks OK.
- SPFx calls
POST /api/alertmngr/createwith the fullIAlertInfopayload includingLastChangedTokencaptured via PnPjs. - Backend validates the Bearer token, upserts
AlertInfoto SQL, and checks for existing webhook subscriptions viaDBWebHookEntriesManager. - If no webhook exists, acquires OBO token and calls
WebhookService.RegisterWebhookAsync()withclientState = "AlertID-{id}". - Returns
201 Createdwith persistedAlertInfoincluding database ID andSubscriptionID.
Change detection via SharePoint webhook
- A list item is added, updated, or deleted in SharePoint.
- SharePoint sends
POST /api/webhookwith aSpNotificationPayloadcontaining the subscription ID,clientState, list GUID, tenant ID, site URL, and web ID. SharePointWebhookReceivervalidates thatclientStatestarts with"AlertID-", then stores aTriggerEventin SQL (fire-and-forget).- On the first webhook confirmation,
SubscriptionIDis updated on theAlertInforecord. - Returns
200 OKimmediately — SharePoint requires a response within a few seconds.
Scheduled notification processing
SchedulerNotificationfires every 5 minutes.- Fetches all active
AlertInforecords and unprocessedTriggerEventsper tenant. - Events are deduplicated by list GUID before processing.
- For each
AlertInfowith a matchingTriggerEvent,ChangeManager.ProcessNotification()runs:- Acquires SharePoint access token via MSAL with certificate credentials.
- Retrieves changed items from SharePoint using CSOM, using
LastChangedTokenas the delta cursor. - Applies
ChangeTypeandAlertTypefilters. - Dispatches notification: Email →
MailManager→ Graph, Teams →TeamsManager→ Bot Connector, SMS → HTTP. - Writes
SPNotifyLogand advancesLastChangedTokenon the alert.
- Processed
TriggerEventsare marked complete and deleted.
Log viewing
- User opens "My Notifications" → "Log entries" tab.
- SPFx calls
GET /api/alertlog/{alertId}; backend returnsISPNotifyLog[]. - A
DetailsListdisplays channel type, recipients, item counts, and timestamp. - Clicking a log row opens a dialog rendering the full HTML email body.
API contract
All frontend-to-backend communication uses these ten REST endpoints. Every request must carry a Bearer token and the SPTenantID header.
| Endpoint | Method | Frontend caller | Request body | Response |
|---|---|---|---|---|
| /api/alertmngr/create | POST | AlertService.createAlert() | IAlertInfo | 201 |
| /api/alertmngr/update | POST | AlertService.updateAlert() | IAlertInfo | 200 |
| /api/alertmngr/delete | POST | AlertService.deleteAlert() | { ID, ListId } | 204 |
| /api/alerts4list/{listGuid} | GET | AlertService.getAlerts() | — | 200 |
| /api/alerts/{id} | GET | AlertService.getAlert() | — | 200 |
| /api/alertlog/{alertId} | GET | AlertService.getAlertLogs() | — | 200 |
| /api/configmngr/create | POST | ConfigurationService.createConfiguration() | ITenantConfiguration | 201 |
| /api/configmngr/update | POST | ConfigurationService.updateConfiguration() | ITenantConfiguration | 200 |
| /api/configmngr/delete | POST | ConfigurationService.deleteConfiguration() | { ID } | 204 |
| /api/configmngr/tenant/{tenantId} | GET | ConfigurationService.getConfiguration() | — | 200 |
POST /api/webhook is not called by the frontend — it is called directly by SharePoint's subscription infrastructure. It validates clientState instead of a Bearer token.Shared data models
TypeScript frontend interfaces and C# backend classes mirror each other exactly. Enum integer values are identical across both codebases — no transformation layer needed.
IAlertInfo ↔ AlertInfo
| TypeScript (IAlertInfo) | C# (AlertInfo) | Type |
|---|---|---|
| ID | ID | number / long |
| AlertTitle | AlertTitle | string |
| SendAlertsTo | SendAlertsTo | string[] / List<string> |
| DeliveryMethod | DeliveryMethod | DeliveryMethod enum |
| AlertType | AlertType | AlertType enum |
| ChangeType | ChangeType | ChangeType enum |
| FilterViewId | FilterViewId | string? |
| AlertFrequency | AlertFrequency | AlertFrequency enum |
| SummaryDay | SummaryDay | number? / int? |
| SummaryTime | SummaryTime | string? |
| ExpirationDate | ExpirationDate | Date? / DateTime? |
| IsAlertActive | IsAlertActive | boolean / bool |
| TeamsID | TeamsID | string? |
| ChannelID | ChannelID | string? |
| ListId | ListId | string (GUID) |
| ListName | ListName | string |
| SiteName | SiteName | string |
| SPSiteUrl | SPSiteUrl | string? |
| TenantID | TenantID | string? (GUID) |
| UserID | UserID | string? |
| SubscriptionID | SubscriptionID | string? (GUID) |
| LastChangedToken | LastChangedToken | string? |
| LastNotificationProcessed | LastNotificationProcessed | Date? / DateTime? |
| NextNotificationToProcess | NextNotificationToProcess | Date? / DateTime? |
Enums
All enums are shared verbatim between TypeScript and C#. Integer values are serialised over the wire and must remain stable across deployments.
Notification channels
The backend supports three outbound delivery channels, selected per-alert based on DeliveryMethod and executed by ChangeManager.ProcessNotification() after the CSOM diff confirms qualifying changes.
POST /v1.0/users/{sender}/sendMail
Sender
EMailFrom must be a licensed Exchange mailbox in the tenant.
Content
HTML body built by MailManager, summarising added / updated / deleted items.
Recipients
Display names resolved via Microsoft Graph before sending.
TenantConfiguration.
Content
Plain-text message. SendUrlInSms flag appends the item URL.
Provider
Provider-agnostic — any HTTP-based gateway can be configured per tenant.
TeamsBotAppID / TeamsBotPassword.
Content
TeamsManager formats as an Adaptive Card posted to the target channel.
Target
TeamsID + ChannelID selected at alert-creation via Graph me/joinedTeams.