WebPush Notifications
WebPush notifications display a browser notification at the operating system level, even when the Lagoon application is not in the foreground. On desktop, the browser must typically be running to display received notifications; if notifications arrive while it is closed, they display when it opens again. Chrome uses system notifications, while Firefox uses its own display, which is more limited for images. On Android, notifications can display even if the browser is closed; if the device is offline, they display when connectivity returns.
Lagoon relies on the Lagoon.WebPush module, a client-side service worker, and the VAPID standard to authenticate the application with the browser's push service.
The user must explicitly accept notifications. It is not possible to force a subscription. If the user logs out, Lagoon removes the WebPush subscription on the browser so they no longer receive notifications after logout.
Prerequisites & Installation
Package and service worker
Add the Lagoon.WebPush package on the server side. The client must activate a service worker because subscription and receiving push, notificationclick, and pushsubscriptionchange events go through the browser.
In older Lagoon applications, service worker activation was often tied to offline mode. WebPush requires at minimum an active service worker on the client side.
VAPID keys
The configuration expects three keys:
{
"App": {
"Name": "Lagoon",
"VapidPublicKey": "BJBj9SRrYF6..."
},
"VapidPrivateKey": "ry-6f4nOC...",
"VapidSubject": "mailto:contact@example.com"
}
App:VapidPublicKeyis shared with the client and passed to the browser'sPushManager.VapidPrivateKeystays on the server side.VapidSubjectmust be an absolute URL or amailto:address.
Keys can be generated using the VAPID key generator available in the debug menu in development environment: open the VAPID key generator, run the generation, then copy the public key and private key to the configuration. The same generation is exposed on the server side via:
VapidDetails GenerateVapidKeys();
The VapidDetails class exposes the following properties:
public string Subject { get; set; }
public string PublicKey { get; set; }
public string PrivateKey { get; set; }
public long Expiration { get; set; } = -1;
Never share the VAPID private key and do not store it on the client side.
DI registration
Register WebPush services on the server side:
services.AddWebPush();
The actual extension method is:
public static IServiceCollection AddWebPush(this IServiceCollection services)
It registers:
| Service | Implementation |
|---|---|
IWebPushNotificationManager | WebPushNotificationManager |
IWebPushSubscriptionManager | WebPushSubscriptionManager |
Client-side subscription
Subscription is handled by App.ServiceWorkerManager. The useful public methods are:
public async Task<bool> SubscribeToPushNotificationAsync()
public async Task UnsubscribeToPushNotificationAsync(bool deleteServerSideRegistration = false)
public Task DisplayWebPushSubscriptionPopupAsync()
public bool IsPushNotificationGranted()
public bool IsServiceWorkerActive()
public bool IsWebPushAvailable()
SubscribeToPushNotificationAsync() checks App:VapidPublicKey, calls the browser's PushManager with the VAPID public key, then sends the subscription to the server. The exchanged object contains:
public string Endpoint { get; set; }
public string P256dh { get; set; }
public string Auth { get; set; }
public string OldEndpoint { get; set; }
public string UserId { get; set; }
public bool IsValid { get; }
Example of a screen offering subscription:
@inherits LgPage
<LgButton Text="#EnableWebPush" OnClick="SubscribeAsync" />
<LgButton Text="#DisableWebPush" Kind="ButtonKind.Secondary" OnClick="UnsubscribeAsync" />
public partial class NotificationSettingsPage : LgPage
{
private async Task SubscribeAsync(ActionEventArgs args)
{
try
{
// Triggers the browser permission request then registers the subscription on the server.
bool subscribed = await App.ServiceWorkerManager.SubscribeToPushNotificationAsync();
if (!subscribed)
{
ShowWarning("#WebPushSubscriptionBlocked");
}
}
catch (Exception ex)
{
ShowException(ex);
}
}
private async Task UnsubscribeAsync(ActionEventArgs args)
{
try
{
// Removes the browser subscription and also requests deletion on the server.
await App.ServiceWorkerManager.UnsubscribeToPushNotificationAsync(true);
ShowSuccess("#WebPushSubscriptionDeleted");
}
catch (Exception ex)
{
ShowException(ex);
}
}
}
The service worker displays the notification with showNotification, navigates to the route placed in Data on click, and updates the server subscription when the browser triggers pushsubscriptionchange.
Server-side sending
The main sending service is IWebPushNotificationManager:
Task<bool> SendNotificationAsync(string userId, string title, string message, string icon = null);
Task<bool> SendNotificationAsync(string userId, PushNotificationMessage message);
IEnumerable<string> GetUserWithNotification();
SendNotificationAsync returns true if at least one user subscription received the notification. Expired or invalid subscriptions are automatically deleted when the push service responds with Gone or NotFound.
Example of a server service:
public class UserAlertService
{
private readonly IWebPushNotificationManager _webPushNotificationManager;
public UserAlertService(IWebPushNotificationManager webPushNotificationManager)
{
_webPushNotificationManager = webPushNotificationManager;
}
public Task<bool> SendImportFinishedAsync(string userId, string title, string body, string targetRoute)
{
PushNotificationMessage message = new()
{
Title = title,
Body = body,
Icon = "icon-160.png",
RequireInteraction = true,
Data = targetRoute
};
// Sends the same message to all subscribed browsers of the user.
return _webPushNotificationManager.SendNotificationAsync(userId, message);
}
}
PushNotificationMessage serializes the options passed to the service worker:
public string Title { get; set; }
public List<PushNotificationAction> Badge { get; set; }
public string Body { get; set; }
public string Data { get; set; }
public string Icon { get; set; }
public string Image { get; set; }
public bool Renotify { get; set; }
public bool RequireInteraction { get; set; }
public string Tag { get; set; }
public bool Silent { get; set; }
public int[] Vibrate { get; set; } = new int[] { 300, 100, 400 };
public override string ToString();
Common options:
Title: notification title.Body: displayed content. Keep it brief, as visible length depends on the system.Icon: icon displayed next to the notification.Image: optional image displayed by compatible browsers.RequireInteraction: iftrue, the notification does not hide automatically.Data: Lagoon client route used when the notification is clicked.
Link with user notifications
When the application uses real-time user notifications and a WebPush service worker is active, user notifications can also trigger a WebPush notification to alert the user outside the active tab.
Subscription management
IWebPushSubscriptionManager centralizes subscription persistence:
void SaveRegistration(WebPushSubscription reg);
void UpdateRegistration(WebPushSubscription reg);
void DeleteRegistration(string userId, string endpoint);
VapidDetails GenerateVapidKeys();
Lagoon's internal endpoints use these methods to:
- save an authenticated subscription after
SubscribeToPushNotificationAsync(); - update a subscription renewed by the browser;
- delete the current user's subscription when unsubscribing;
- generate VAPID keys in development environment.
The same person can have multiple subscriptions, for example a desktop browser and a mobile browser.