Skip to main content

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.

warning

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.

Note

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:

appsettings.json
{
"App": {
"Name": "Lagoon",
"VapidPublicKey": "BJBj9SRrYF6..."
},
"VapidPrivateKey": "ry-6f4nOC...",
"VapidSubject": "mailto:contact@example.com"
}
  • App:VapidPublicKey is shared with the client and passed to the browser's PushManager.
  • VapidPrivateKey stays on the server side.
  • VapidSubject must be an absolute URL or a mailto: 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;
Security

Never share the VAPID private key and do not store it on the client side.

DI registration​

Register WebPush services on the server side:

Registry.cs
services.AddWebPush();

The actual extension method is:

public static IServiceCollection AddWebPush(this IServiceCollection services)

It registers:

ServiceImplementation
IWebPushNotificationManagerWebPushNotificationManager
IWebPushSubscriptionManagerWebPushSubscriptionManager

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:

NotificationSettingsPage.razor
@inherits LgPage

<LgButton Text="#EnableWebPush" OnClick="SubscribeAsync" />
<LgButton Text="#DisableWebPush" Kind="ButtonKind.Secondary" OnClick="UnsubscribeAsync" />
NotificationSettingsPage.razor.cs
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:

UserAlertService.cs
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 : if true, the notification does not hide automatically.
  • Data : Lagoon client route used when the notification is clicked.

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.

See also​