How to send web push notifications in Java
Tags: java, webdev, springboot, typescript
Published: 2021-05-27 23:42:24

---
title: How to send web push notifications in Java
published: true
description: Learn how to subscribe to web push notifications in the browser and send push messages from a Java server.
tags: java, webdev, springboot, typescript
canonical_url: https://hilla.dev/blog/send-web-push-notifications-java/
//cover_image: https://direct_url_to_image.jpg
---

[Web push notifications](https://developers.google.com/web/fundamentals/push-notifications) are a way of informing your app users when something important has happened. 

Users can receive web push notifications even when they are not actively using your application, for instance, if the app is open in a background tab or even if it's not open. 

![A notification message above an open browser window.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e1vxfxy2bt5nsgbhhyuc.jpg)
 

Push notifications are [widely supported](https://caniuse.com/push-api) by all browsers except for Safari: 78% of web users use a browser that supports them.

In this tutorial, I'll show you **how to subscribe to notifications in the browser** and **how to send notifications from a Java server**. 

## Video version 

{% youtube M5sbGvW3S4I %}

## A bit of background: how web push notifications work

Web push notifications rely on two web standards: [Notification API](https://developer.mozilla.org/en-US/docs/Web/API/notification) and [Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) (which in turn uses [ServiceWorker](https://github.com/marcushellberg/fusion-push-notifications)). They require HTTPS to work. 

### Subscribing to push notifications

- The server shares its public key with the browser
- The browser uses the public key to subscribe to a push service (each browser have their own)
- The push service returns a subscription with a unique endpoint URL that can be used to send push messages
- The subscription is saved to the server

![A diagram showing a server, browser, and endpoint. The server has a public and private key. The browser uses the public key to subscribe to the endpoint.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rb3m32j83bxsnshbiv27.png)
<figcaption>Image courtesy of <a href="https://developers.google.com/web/fundamentals/push-notifications/subscribing-a-user">Google Web Fundamentals</a></figcaption>

### Sending push notifications

- The server signs an authorization header with its private key
- The server sends the message to the unique endpoint URL 
- The push server decrypts the auth header 
- The push server sends the message to the device/browser

![A diagram showing a server signing a message with the private key, sending it to the endpoint, which decrypts the header and sends the message to the device](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0vju1r9czxzdx1zijvya.png)
<figcaption>Image courtesy of <a href="https://developers.google.com/web/fundamentals/push-notifications/subscribing-a-user">Google Web Fundamentals</a></figcaption>

## Setup project and generate VAPID keys

I'm using [Hilla](https://hilla.dev) for this example. Hilla uses [Spring Boot](https://spring.io/projects/spring-boot) on the backend and [Lit](https://lit.dev) on the frontend. 

**I will only cover the key steps here. You can find the complete source code on [GitHub](https://github.com/marcushellberg/hilla-push-notifications).**

You can create a new Fusion project with the Vaadin CLI:

```
npx @vaadin/cli init --hilla push-app
```

Create a set of VAPID keys with the `web-push` npm package. 

```
npx web-push generate-vapid-keys
```

Create a new file `.env` in the project directory and use it to store the keys. Add it to your `.gitignore` so you don't accidentally publish it.

```
export VAPID_PUBLIC_KEY=BAwZxXp0K....
export VAPID_PRIVATE_KEY=1HLNMKEE....
```

Add the [Java WebPush](https://github.com/web-push-libs/webpush-java) and [BouncyCastle](https://www.bouncycastle.org/) library dependency to `pom.xml`:

```xml
<dependency>
    <groupId>nl.martijndwars</groupId>
    <artifactId>web-push</artifactId>
    <version>5.1.1</version>
</dependency>
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.70</version>
</dependency>
```

Load the environment file and start the app:

```
source .env
mvn
```

## Create a Java service for handling subscriptions and sending notifications

Create a new Spring Boot service, `MessageService.java`. This service will read in the keys and 

```java
package com.example.application;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.Security;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.jose4j.lang.JoseException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import nl.martijndwars.webpush.Notification;
import nl.martijndwars.webpush.PushService;
import nl.martijndwars.webpush.Subscription;

@Service
public class MessageService {

  @Value("${vapid.public.key}")
  private String publicKey;
  @Value("${vapid.private.key}")
  private String privateKey;

  private PushService pushService;
  private List<Subscription> subscriptions = new ArrayList<>();

  @PostConstruct
  private void init() throws GeneralSecurityException {
    Security.addProvider(new BouncyCastleProvider());
    pushService = new PushService(publicKey, privateKey);
  }

  public String getPublicKey() {
    return publicKey;
  }

  public void subscribe(Subscription subscription) {
    System.out.println("Subscribed to " + subscription.endpoint);
    this.subscriptions.add(subscription);
  }

  public void unsubscribe(String endpoint) {
    System.out.println("Unsubscribed from " + endpoint);
    subscriptions = subscriptions.stream().filter(s -> !endpoint.equals(s.endpoint))
        .collect(Collectors.toList());
  }

  public void sendNotification(Subscription subscription, String messageJson) {
    try {
      pushService.send(new Notification(subscription, messageJson));
    } catch (GeneralSecurityException | IOException | JoseException | ExecutionException
        | InterruptedException e) {
      e.printStackTrace();
    }
  }

  @Scheduled(fixedRate = 15000)
  private void sendNotifications() {
    System.out.println("Sending notifications to all subscribers");

    var json = """
        {
          "title": "Server says hello!",
          "body": "It is now: %s"
        }
        """;

    subscriptions.forEach(subscription -> {
      sendNotification(subscription, String.format(json, LocalTime.now()));
    });
  }
}
```

Some key things to note: 

- The `@Value("${vapid.public.key}")` annotation reads the environment variables into the fields.
- The service stores the subscriptions in a `List`. In a more practical application, you would keep them in a database along with the user.
- You send push notifications with `pushService.send(new Notification(subscription, messageJson))`. The payload can also be plain text, but JSON is more flexible.
- The service sends out a notification to all subscribers every 15 seconds, containing the current time.

## Create an Endpoint for accessing the server

Next, you need a way to access the server from the browser. In Vaadin Fusion, you do this by defining an [Endpoint](https://vaadin.com/docs/latest/fusion/application/accessing-backend). The endpoint will generate TypeScript types and TS accessor methods you can use in the client code. 

```java
package com.example.application;

import com.vaadin.flow.server.auth.AnonymousAllowed;
import dev.hilla.Endpoint;
import dev.hilla.Nonnull;
import nl.martijndwars.webpush.Subscription;

@Endpoint
@AnonymousAllowed
public class MessageEndpoint {

  private MessageService messageService;

  public MessageEndpoint(MessageService messageService) {
    this.messageService = messageService;
  }

  public @Nonnull String getPublicKey() {
    return messageService.getPublicKey();
  }

  public void subscribe(Subscription subscription) {
    messageService.subscribe(subscription);
  }

  public void unsubscribe(String endpoint) {
    messageService.unsubscribe(endpoint);
  }
}

```

Some things to note: 
- Endpoints are secured by default. You can allow anonymous access with `@AnonymousAllowed`.
- The endpoint injects the message service and delegates subscribing and unsubscribing to it. 

## Subscribe to notifications in the browser

Create a view for subscribing to notifications. The [LitElement](https://lit.dev) component keeps track of two pieces of state: 
- whether the user has allowed notifications
- whether the user has an existing push subscription

```typescript
import { html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import '@vaadin/button';
import { View } from '../view';
import { MessageEndpoint } from 'Frontend/generated/endpoints';

@customElement('notifications-view')
export class NotificationsView extends View {
  @state() denied = Notification.permission === 'denied';
  @state() subscribed = false;

  render() {
    return html`
      <h1>Web Push Notifications 📣</h1>

      ${this.denied
        ? html` <b> You have blocked notifications. You need to manually enable them in your browser. </b> `
        : ''}
      ${this.subscribed
        ? html`
            <p>Hooray! You are subscribed to receive notifications 🙌</p>
            <vaadin-button theme="error" @click=${this.unsubscribe}>Unsubscribe</vaadin-button>
          `
        : html`
            <p>You are not yet subscribed to receive notifications.</p>

            <vaadin-button theme="primary" @click=${this.subscribe}>Subscribe</vaadin-button>
          `}
    `;
  }

  async firstUpdated() {
    const registration = await navigator.serviceWorker.getRegistration();
    this.subscribed = !!(await registration?.pushManager.getSubscription());
  }

  async subscribe() {
    const notificationPermission = await Notification.requestPermission();

    if (notificationPermission === 'granted') {
      const publicKey = await MessageEndpoint.getPublicKey();
      const registration = await navigator.serviceWorker.getRegistration();
      const subscription = await registration?.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: this.urlB64ToUint8Array(publicKey),
      });

      if (subscription) {
        this.subscribed = true;
        // Serialize keys uint8array -> base64
        MessageEndpoint.subscribe(JSON.parse(JSON.stringify(subscription)));
      }
    } else {
      this.denied = true;
    }
  }

  async unsubscribe() {
    const registration = await navigator.serviceWorker.getRegistration();
    const subscription = await registration?.pushManager.getSubscription();
    if (subscription) {
      await subscription.unsubscribe();
      await MessageEndpoint.unsubscribe(subscription.endpoint);
      this.subscribed = false;
    }
  }

  private urlB64ToUint8Array(base64String: string) {
    const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
    const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);
    for (let i = 0; i < rawData.length; ++i) {
      outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
  }
}
```

The important part here is the `subscribe()`-method. Here is what it does:

1. Asks the user for permission to show notifications with `Notification.requestPermission()`. The answer will be "granted" or "denied". **NOTE: If the user declines, you cannot ask them again. Be sure to only prompt the user when they expect and want notifications.**
2. If the user grants permission, fetch the public key from the server and use the ServiceWorker PushManager to subscribe to notifications. The `applicationServerKey` is a Uint8Array containing the public key. You need to convert it with the included method. (Not the most convenient API 🤷‍♂️)
3. If the subscription succeeds, send it to the server.

## Handle incoming push messages in the ServiceWorker

Once you are subscribed to notifications, the server will send out a notification every 15 seconds. 

Override the Vaadin generated ServiceWorker by copying `target/sw.ts` -> `frontend/sw.ts`.

Add the following two listeners to `sw.ts`:

```typescript
self.addEventListener('push', (e) => {
  const data = e.data?.json();
  if (data) {
    self.registration.showNotification(data.title, {
      body: data.body,
    });
  }
});

self.addEventListener('notificationclick', (e) => {
  e.notification.close();
  e.waitUntil(focusOrOpenWindow());
});

async function focusOrOpenWindow() {
  const url = new URL('/', self.location.origin).href;

  const allWindows = await self.clients.matchAll({
    type: 'window',
  });
  const appWindow = allWindows.find((w) => w.url === url);

  if (appWindow) {
    return appWindow.focus();
  } else {
    return self.clients.openWindow(url);
  }
}
```

- The `fetch` listener gets called when a new message comes in. Read the event `data` property as JSON to access the message payload.
  - Use `self.registration.showNotification()` to show a notification using the message data. 
- The `notificationclick` listener gets called when you click on the notification.
  - Close the notification. 
  - See if the user has an open tab application tab. If they do, focus it. If they don't, open a new window.

## Source code

You can find the complete source code on my GitHub: https://github.com/marcushellberg/hilla-push-notifications.