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

Design intent. The native SharePoint alert feature has been retired by Microsoft. SP Notify fully owns every subscription, delivery, and log record — giving teams complete control over formatting, frequency, and channel routing that the retired system never provided.

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.

1
User opens SharePoint list page SharePoint loads the SPFx ListViewCommandSet extension and renders the "My Notifications" toolbar command.
2
SPFx requests an access token The extension calls AadHttpClient, which uses SharePoint's token brokering to request a JWT for api://<client-id> from Azure AD. No redirect or popup required.
3
Azure AD returns a signed JWT The access token is scoped to the registered application and includes the user's identity claims.
4
SPFx calls the backend with two required headers Authorization: Bearer <token> carries the JWT. SPTenantID: <tenant-guid> identifies the calling tenant so the backend can resolve the correct OIDC metadata endpoint.
5
Backend fetches OIDC signing keys (cached) The backend uses TokenValidatorCache to retrieve and cache the tenant's OpenID Connect metadata and signing keys, avoiding repeated round-trips on subsequent requests.
6
JWT signature and lifetime are validated The backend validates the token signature against the cached signing keys and checks expiry. On success the function proceeds; on failure it returns 401.
The 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

[DS.SP.Notify.TenantConfiguration]
TenantId // Primary key — Azure AD tenant GUID
ClientId // Entra app registration client ID
ClientSecret // Client secret or certificate thumbprint
Certificate // X.509 certificate for MSAL
SiteUrl // SharePoint site collection URL
WebhookNotificationUrl // Public HTTPS URL for SP webhook delivery
EMailFrom // Licensed mailbox for Graph sendMail
TeamsBotAppID // Bot Framework app ID
TeamsBotPassword // Bot Framework app password
TeamsTrafficManagerServiceUrl // Bot Connector endpoint

Isolation mechanisms

  • Every HTTP request carries the SPTenantID header, which the backend uses to scope all database reads and writes to that tenant only.
  • TokenValidatorCache maintains separate per-tenant OIDC validation parameters (signing keys, issuer URI).
  • SchedulerNotification iterates all registered tenants independently, instantiating a separate ChangeManager and credential set per tenant per run.
  • All SQL queries include a TenantID predicate — there are no cross-tenant queries.

Onboarding a new tenant

1
Register an Entra app Create an app registration in the target tenant's Azure AD. Grant required Graph and SharePoint permissions. Upload a certificate for MSAL authentication.
2
Deploy the SPFx package Upload the .sppkg bundle to the tenant's SharePoint App Catalog and approve the API permissions so the extension can acquire tokens.
3
Create the tenant record Use the 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.

Phase 1

Alert creation

  1. User clicks "My Notifications" in the SharePoint list toolbar.
  2. SPFx loads AlertFormPanel and calls GET /api/alerts4list/{listId} to pre-populate existing alerts (scoped to the user).
  3. User fills in the alert form and clicks OK.
  4. SPFx calls POST /api/alertmngr/create with the full IAlertInfo payload including LastChangedToken captured via PnPjs.
  5. Backend validates the Bearer token, upserts AlertInfo to SQL, and checks for existing webhook subscriptions via DBWebHookEntriesManager.
  6. If no webhook exists, acquires OBO token and calls WebhookService.RegisterWebhookAsync() with clientState = "AlertID-{id}".
  7. Returns 201 Created with persisted AlertInfo including database ID and SubscriptionID.
Phase 2

Change detection via SharePoint webhook

  1. A list item is added, updated, or deleted in SharePoint.
  2. SharePoint sends POST /api/webhook with a SpNotificationPayload containing the subscription ID, clientState, list GUID, tenant ID, site URL, and web ID.
  3. SharePointWebhookReceiver validates that clientState starts with "AlertID-", then stores a TriggerEvent in SQL (fire-and-forget).
  4. On the first webhook confirmation, SubscriptionID is updated on the AlertInfo record.
  5. Returns 200 OK immediately — SharePoint requires a response within a few seconds.
Phase 3

Scheduled notification processing

  1. SchedulerNotification fires every 5 minutes.
  2. Fetches all active AlertInfo records and unprocessed TriggerEvents per tenant.
  3. Events are deduplicated by list GUID before processing.
  4. For each AlertInfo with a matching TriggerEvent, ChangeManager.ProcessNotification() runs:
    • Acquires SharePoint access token via MSAL with certificate credentials.
    • Retrieves changed items from SharePoint using CSOM, using LastChangedToken as the delta cursor.
    • Applies ChangeType and AlertType filters.
    • Dispatches notification: Email → MailManager → Graph, Teams → TeamsManager → Bot Connector, SMS → HTTP.
    • Writes SPNotifyLog and advances LastChangedToken on the alert.
  5. Processed TriggerEvents are marked complete and deleted.
Phase 4

Log viewing

  1. User opens "My Notifications" → "Log entries" tab.
  2. SPFx calls GET /api/alertlog/{alertId}; backend returns ISPNotifyLog[].
  3. A DetailsList displays channel type, recipients, item counts, and timestamp.
  4. 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/createPOSTAlertService.createAlert()IAlertInfo201
/api/alertmngr/updatePOSTAlertService.updateAlert()IAlertInfo200
/api/alertmngr/deletePOSTAlertService.deleteAlert(){ ID, ListId }204
/api/alerts4list/{listGuid}GETAlertService.getAlerts()200
/api/alerts/{id}GETAlertService.getAlert()200
/api/alertlog/{alertId}GETAlertService.getAlertLogs()200
/api/configmngr/createPOSTConfigurationService.createConfiguration()ITenantConfiguration201
/api/configmngr/updatePOSTConfigurationService.updateConfiguration()ITenantConfiguration200
/api/configmngr/deletePOSTConfigurationService.deleteConfiguration(){ ID }204
/api/configmngr/tenant/{tenantId}GETConfigurationService.getConfiguration()200
Webhook endpoint note. 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
IDIDnumber / long
AlertTitleAlertTitlestring
SendAlertsToSendAlertsTostring[] / List<string>
DeliveryMethodDeliveryMethodDeliveryMethod enum
AlertTypeAlertTypeAlertType enum
ChangeTypeChangeTypeChangeType enum
FilterViewIdFilterViewIdstring?
AlertFrequencyAlertFrequencyAlertFrequency enum
SummaryDaySummaryDaynumber? / int?
SummaryTimeSummaryTimestring?
ExpirationDateExpirationDateDate? / DateTime?
IsAlertActiveIsAlertActiveboolean / bool
TeamsIDTeamsIDstring?
ChannelIDChannelIDstring?
ListIdListIdstring (GUID)
ListNameListNamestring
SiteNameSiteNamestring
SPSiteUrlSPSiteUrlstring?
TenantIDTenantIDstring? (GUID)
UserIDUserIDstring?
SubscriptionIDSubscriptionIDstring? (GUID)
LastChangedTokenLastChangedTokenstring?
LastNotificationProcessedLastNotificationProcessedDate? / DateTime?
NextNotificationToProcessNextNotificationToProcessDate? / DateTime?

Enums

All enums are shared verbatim between TypeScript and C#. Integer values are serialised over the wire and must remain stable across deployments.

DeliveryMethod
0Email
1SMS
2Teams
AlertType
0All
1Updated
2Added
3Removed
AlertFrequency
0Immediate
1Daily summary
2Weekly summary
ChangeType
0Anything changes
1Someone else changes
2Someone else changes (created by me)
3Someone else changes (modified by me)
4Item in view changes

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.

💬 SMS
Delivery HTTP POST to a configured SMS gateway endpoint in TenantConfiguration. Content Plain-text message. SendUrlInSms flag appends the item URL. Provider Provider-agnostic — any HTTP-based gateway can be configured per tenant.
🟣 Teams
Delivery Teams Bot Connector using 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.