How to setup basic web push notification functionality using a Flask backend

Web browsers have become increasingly very powerful over the past few years. Many features which were earlier offered only by mobile apps are now available on web as well. This has primarily been due to Google’s investments in Chrome browser (and Mozilla’s work on Firefox as well) and some allied technologies like Progressive Web Apps

Service Workers are the core piece of the technology that enabled PWAs to compete with native apps in terms of the functionality offered. You can get a good introduction to how these service workers work from this tutorial created by google and this MDN article. As mentioned in those articles, service workers can do a bunch of great things like

  1. Act as a network proxy - caching various assets required by the site and letting a basic site be loaded even without network connectivity

  2. Sending data to the server even after the user has navigated away from the page - thus making background sync possible.

  3. Receive push notifications from the server and display them to the user.

In this post we will just stick to the last mentioned feature - Web Push Notifications. It is easy to add support for the same by integrating third party tools like WebEngage, OneSignal, Clevertap etc. This blog itself uses OneSignal for managing push subscriptions. You can see a bell shaped icon on the bottom right. On clicking it, you will see a dialog asking you permission to allow the site to send you notifications. If you click on “Allow”, I will be able to send you some neat and crisp notifications whenever I write something interesting on this blog.

While these third party tools are sufficient for most purposes, if we intend to incorporate push notifications as a significant part of the user experience, it will be worthwhile to implement it ourselves. Google web fundamentals course has a detailed tutorial here on this topic. There is also a codelabs project which provides a demo project which can be cloned and experimented with. And then there is this Web Push Book which is probably the most extensive tutorial available on the subject.

While these tutorials are quite detailed, there is also a need for a step by step tutorial that starts from scratch and progressively builds the features. This series intends to be that. In the first post of the series, we will just set up a bare minimal application with Web Push support just to demonstrate how to get it up and running. In the subsequent parts, we will improvise the user experience and add more features.

Before we proceed further, you should read through this post to get an understanding of how the technology works. If you are short of time, here is a concise version -

  • Javascript is single threaded. This means that the when a website is loaded, the main thread will always be occupied in rendering and handling user interactions. But this limits the user experience that we can provide to the users. There was a need for threads which can run in the background without disturbing the user interface.

  • Web workers were developed to satisfy this requirement.

  • Service workers are a type of web workers, via which we can instruct the browser to do certain specific background tasks.

  • One of the tasks that they can do is to listen for Push notifications sent by the server which can then be displayed to the user.

  • Chrome pioneered support for this feature. Firefox incorporates it as well now.

So, with this basic understanding of WebPush, let’s dive into the code now.

Our hero - The Service Worker

Let’s first see what a service worker js file looks like. A side-note - I have taken the scripts used in the Google Codelabs project and modified them to keep only what is essential for a simple demo. .

    'use strict';

    /* eslint-enable max-len */

    self.addEventListener('install', function(event) {
      console.log('Service Worker installing.');

    self.addEventListener('activate', function(event) {
      console.log('Service Worker activating.');

    self.addEventListener('push', function(event) {
      console.log('[Service Worker] Push Received.');
      const pushData =;
      console.log("[Service Worker] Push received this data - ", pushData);
      let data, title, body;
      try {
        data = JSON.parse(pushData);
        title = data.title;
        body = data.body;
      } catch(e) {
        title = "Untitled";
        body = pushData;
      const options = {
        body: body
      console.log(title, options);

        self.registration.showNotification(title, options)

The code should be easy to understand for anyone with a basic javascript knowledge. The main unusual feature in the above code is the keyword self. It is a reference to the ServiceWorkerGlobalScope which is an interface representing the scope of the service worker. Since the service worker does not have a browsing context, this self reference provides the interface for various functionalities which are usually provided by the window object in the browsing context. Just like we would do window.addEventListener, here the event handlers of the service worker are defined using self.addEventListener

In the above code, we have listed 3 event handlers. The ones handling install and activate are just logging the events for our debugging purposes. The push event handler is also simple to understand. It reads the text data sent with the event, checks if it is a JSON format, and if so extracts a title and body from it. If it is not in JSON format, it just assumes that the entire string is to be used as the body, with title set as “Untitled”. As we will see below, this will help us debug the service worker from the client’s chrome developer tools itself.

Registering the service worker

The above service worker needs to be registered by the website so that the browser knows about it. Here is the registration script which we are going to use - register_service_worker.js

    'use strict';

    function urlB64ToUint8Array(base64String) {
      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;

    function updateSubscriptionOnServer(subscription, apiEndpoint) {
      // TODO: Send subscription to application server

      return fetch(apiEndpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        body: JSON.stringify({
          subscription_json: JSON.stringify(subscription)


    function subscribeUser(swRegistration, applicationServerPublicKey, apiEndpoint) {
      const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
        userVisibleOnly: true,
        applicationServerKey: applicationServerKey
      .then(function(subscription) {
        console.log('User is subscribed.');

        return updateSubscriptionOnServer(subscription, apiEndpoint);

      .then(function(response) {
        if (!response.ok) {
          throw new Error('Bad status code from server.');
        return response.json();
      .then(function(responseData) {
        if (responseData.status!=="success") {
          throw new Error('Bad response from server.');
      .catch(function(err) {
        console.log('Failed to subscribe the user: ', err);

    function registerServiceWorker(serviceWorkerUrl, applicationServerPublicKey, apiEndpoint){
      let swRegistration = null;
      if ('serviceWorker' in navigator && 'PushManager' in window) {
        console.log('Service Worker and Push is supported');

        .then(function(swReg) {
          console.log('Service Worker is registered', swReg);
          subscribeUser(swReg, applicationServerPublicKey, apiEndpoint);

          swRegistration = swReg;
        .catch(function(error) {
          console.error('Service Worker Error', error);
      } else {
        console.warn('Push messaging is not supported');
      return swRegistration;

The last function - registerServiceWorker is the main function which will be invoked to register the service worker. It accepts 3 arguments

  1. serviceWorkerUrl - The url which can be used by the browser to load the service worker. In our case, we will place the service worker js in the static folder of our flask app and the url will hence be /static/service_worker.js

  2. The application server public key - The second argument is the vapid public key. This is the public key component of a public key - private key pair which is used to securely authorize the communication between the push server and the push service running on the client. The mechanism is explained in more detail here.

  3. apiEndpoint - Finally the third argument is the api end-point which will be used to save the push subscriptions generated by the clients. In our example we will define an endpoint /api/push-subscriptions which will do this job.

The registerServiceWorker function calls the subscribeUser function which in turn calls the swRegistration.pushManager.subscribe method which generates the unique endpoint which will be used by the push server to send push notifications to the client. A side-effect of the .subscribe function call is that it will request permission from the user for showing push notifications, if the user has not yet allowed it.

Push Notification Permission

This is sufficient for the sake of this simple example. But a good user experience requires that we control when and how this request alert is displayed by the browser. That can indeed be done using the Notification.requestPermission() method described here. But for the sake of brevity, let’s keep that out of this tutorial’s scope as the primary aim of this first post in the series is just to get the push notifications working with the least amount of effort.

The urlB64ToUint8Array is just a helper function used to convert the public key to the format required by the pushManager.subscribe function. Now, the Push Subscription json generated by the swRegistration.pushManager.subscribe function call above looks like this


This json should be stored so that the server can later send the desired push notification to this address. This is done by the updateSubscriptionOnServer function which posts the subscription json to the api endpoint which we will describe below.

Structure of the app

Now that we have reviewed both the service worker script and the script required to register it, we can start discussing the structure of our server which will be used to serve the html pages and the api endpoints.

We are going to build a simple Flask server as the backend and configure it to serve a simple website with a service worker file which can provide Web Push support. The code is available in this repo. The app being used in this first post of the series is under the folder named basic_functionality

The app is structured like this

├── instance
│   └──
├── requirements.txt
├── static
│   ├── register_service_worker.js
│   └── service_worker.js
├── templates
│   ├── admin.html
│   └── index.html

The instance folder will be missing if you clone the github repo. It is a folder which is not meant to be version committed as it will contain the secret keys (to be explained below)

Before we review each of these files one by one, let’s first see how to get the environment ready

Setting up the virtualenv and installing the requirements

We can use virtualenvwrapper to set up the python environment for running the app. Create a virtualenv like this

mkvirtualenv -p python3 simple_webpush

And with the virtualenv active, install the requirements using pip install -r requirements.txt. Refer the doc pages for virtualenv and virtualenvwrapper for more details if you are new to this.

With the environment set, we can start reviewing the server code.

Main server module -

The full code of is available here. We are using a simple Flask app pattern for this example. If you are new to Flask, going through this quickstart tutorial first will help. Note that this simple app structure is suitable only for development purposes. If we intend to deploy the application to production, we need to use a more scaleable architecture. But that is outside the scope of this first post.

Let’s review the code of section by section now.

from flask import Flask, render_template, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from webpush_handler import trigger_push_notifications_for_subscriptions

app = Flask(__name__, instance_relative_config=True)

We are importing the essential flask and sqlalchemy modules. We are also importing a method which will be used for triggering the push notifications (to be discussed below)

The app is created using the flag instance_relative_config set to True. This lets us define some secret config information in a file inside a folder called instance which can be kept out of source control. The next line instructs the app to load the config from a file named which is expected in the instance folder. So just create a folder named instance and create a file named there. We will add the public and private keys required for sending web push notifications here.

Here is how the instance/ should look like

    VAPID_PRIVATE_KEY = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
    VAPID_CLAIM_EMAIL = "[email protected]"

To generate this public key-private key pair, you can use this website - Alternatively you can also use this npm command line utility - web-push.

The next section in takes care of the database layer. The app needs a database connection to store the push subscription information generated by the browser. With Flask we generally use SQLAlchemy as the database ORM layer and FlaskSQLAlchemy as the linker library. You can refer their documentations here and here if you are unfamiliar.

app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite://"
db = SQLAlchemy(app)

class PushSubscription(db.Model):
    id = db.Column(db.Integer, primary_key=True, unique=True)
    subscription_json = db.Column(db.Text, nullable=False)


In the above section, we have done the following

  1. The database connection string sqlite:// instructs SQLAlchemy to use an in-memory sqlite database. Since it is in-memory it is non-persistent and the information will be lost if the server process is shut down. But this is sufficient for the purposes of this simple demo.

  2. In the next line we have initialized the db handle and then we have defined a model named PushSubscription which will be used to store the subscription information. We will review the structure of this json shortly.

  3. The next line db.create_all() takes care of creating the database tables - just one table in this case named push_subscription

With the database configured, we can now proceed to register the required url endpoints. The first route home_page renders the template index.html.

def home_page():
    return render_template("index.html")

The index.html rendered above can just be a bare minimum template which loads the register_service_worker.js script which we had described earlier and then invokes the registerServiceWorker function defined in the script. As required by Flask, we will place this in the templates folder.


        <link rel="icon" href="data:,">
        <h1>Webpush Demo</h1>
        <script type="text/javascript">

We have placed both the js files in the static folder and loaded the register_service_worker.js script directly in the html. That in turn takes care of loading the service worker using the url we have passed as the first argument. The second argument is supposed to be the public key as we discussed earlier when reviewing the registration script. Instead of hardcoding it, we are referring to it using a config variable. Flask will take care of loading this variable from the value we placed in instance/ earlier. And the third argument is supposed to be the api endpoint to which the registration script will upload the subscription_json. We will define it as follows in

@app.route("/api/push-subscriptions", methods=["POST"])
def create_push_subscription():
    json_data = request.get_json()
    subscription = PushSubscription.query.filter_by(
    if subscription is None:
        subscription = PushSubscription(
    return jsonify({
        "status": "success"

As it can be seen, the above api endpoint first checks if there is already a matching PushSubscription object with the same subscription_json, and if so returns it. So when we reload the page, even when the browser resends the same subscription_json due to the registerServiceWorker function being invoked again, it won’t cause any error on the server. If there is no matching subscription_json on the other hand, a new entry is created and returned to the browser.

The setup done so far is enough to check that the service worker works as expected. Chrome developer tools has tools which let us test the service workers from the client itself. So let’s do that first before seeing how to send the push notifications from the server.

Testing the client side functionality

We first start the server by running the flask run command

Flask Run

Once the server starts, open localhost:5000 in the browser. Keep the Developer tools open on the side. In case you haven’t done this before, it is available under “More Tools” section in Chrome menu. Or you can press Ctrl + Shift + I. Once the site loads, you should see a dialog which asks you for notification permissions. Click on Allow.


When clicking on “Allow”, if we inspect the Network tab in Developer tools, we can see a POST request being sent to the API endpoint.


In the response we can see the subscription details.


Now that the registration of service worker is done, let’s test out its notification handler code. In the developer tools, click on the Application tab and click on the Application -> ServiceWorkers section in the side menu.

Application Tab

It shows the registered service worker on the localhost site. The status should show as activated and running. We can now test that this service worker is working as expected on the client side using this devtools interface itself. Type some string in the text-box labeled Push and click on the Push button. You should immediately see a push notification popup on the side. The title shows as “Untitled” because that is how we have coded (Check the service_worker.js code above)


We can also test how the service_worker will show the notification if we send it in the json format that we have coded the service worker to handle.


Thus we have verified that the notification handler in the service worker is working as expected. The next step is to go through the server setup to send the push notifications

Server code to send web push notifications

We will set up a simple admin page to send push notifications. A proper admin dashboard requires user authentication and authorization. But we will leave that for the next posts in this series. Here we will just focus on the code essential for sending out the notifications.

We first define the admin page like this in

def admin_page():
    return render_template("admin.html")

The html is defined as follows with a simple form which allows you to enter a title and body of the push message and submit it.

        <link rel="icon" href="data:,">
        <h1>Trigger a Push Notification</h1>
        <form method="POST" id="trigger-push-form" action="/admin-api/trigger-push-notifications">
                <label for="title">Message Title: </label>
                <input name="title"/>
                <label for="body">Message Body: </label>
                <textarea rows="10" cols="100" name="body"></textarea>
            <input type="submit" value="Trigger Push">
        <script type="text/javascript">
                      type: 'POST',
                      url: $("#trigger-push-form").attr("action"),
                      contentType: 'application/json',
                      processData: false,
                      data: JSON.stringify(
                                    .reduce(function(result, item){
                                                result[] = item.value;
                                                return result;
                                            }, {})
                      success: function(response) {
                        console.log("received response ", response);

On submission, it hits an api endpoint which is defined as follows. This endpoint uses the trigger_push_notifications_for_subscriptions function to send the notification to all the subscribers (only one in our case)

@app.route("/admin-api/trigger-push-notifications", methods=["POST"])
def trigger_push_notifications():
    json_data = request.get_json()
    subscriptions = PushSubscription.query.all()
    results = trigger_push_notifications_for_subscriptions(
    return jsonify({
        "status": "success",
        "result": results

The trigger_push_notifications_for_subscriptions is defined in which we had already imported in

from pywebpush import webpush, WebPushException
import json
from flask import current_app

def trigger_push_notification(push_subscription, title, body):
        response = webpush(
            data=json.dumps({"title": title, "body": body}),
                "sub": "mailto:{}".format(
        return response.ok
    except WebPushException as ex:
        if ex.response and ex.response.json():
            extra = ex.response.json()
            print("Remote service replied with a {}:{}, {}",
        return False

It’s a straightforward invocation of the function defined by the pywebpush library which we had already installed as part of the requirements. We are signing it with the vapid private key which we had copied to the instance/ Since the client already knows the public key, it will be able to validate that the push message is coming from the authorized server only.

Testing out the admin form for sending push notifications

To test this functionality, let’s visit localhost:5000/admin Here, if we fill the title and body and submit, we can see a push notification like this

Admin Push Notification

If you are seeing a notification like above, congratulations you have successfully built a simple website with a very basic web-push notification support.


In the above tutorial we have reviewed how to build a simple app with support for push notifications. We can continue building features on top of this as follows

  1. Improving the push notification handler on client side - by implementing the Notification.requestPermission() method, by adding more options for controlling the appearance and behavior of the notification, adding support for images etc

  2. Modifying the app structure to make it more scaleable and ready to deploy

  3. Use a persistent database for storing the push subscriptions

  4. Implement user authentication and authorization so that the admin form can be opened only for certain users.

  5. Implementing support for user segmentation on the backend, so that we can send the notifications to specific target segments

We will build these features out in the subsequent posts in this series. Please follow this blog to get notified about the subsequent posts.

comments powered by Disqus