Progressive web applications (PWAs) continue to gain widespread attention, acceptance, and compatibility with web browsers due to their native-like attributes. One of the mandatory security considerations to deploying these applications is that they must be hosted securely. Due to this, PWA features will not work on a non-secure URL, i.e. a URL that does not use the secure https:// protocol. In this post, we will create an automated deployment pipeline that deploys our PWA to a secure URL on Firebase.

Prerequisites

To follow this post, a few things are required:

  1. Basic knowledge of Javascript
  2. Node.js installed on your system
  3. An HTTP Server Module installed globally on your system (npm install -g http-server)
  4. A Firebase account
  5. A CircleCI account
  6. A GitHub account

With all these installed and set up, let’s begin the tutorial.

Setting up the demo application

The first task is to create our demo application that we will be deploying to Firebase. Run the following commands to create a directory for the project and go into the root of the directory:

mkdir my-pwa-firebase
cd my-pwa-firebase

Next, let’s create our application home page. Create a file named index.html at the root of the project and paste the following code into it:


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="manifest" href="manifest.json" />
    <meta name="theme-color" content="#db4938" />
    <link rel="stylesheet" type="text/css" href="styles.css" media="all" />

    <title>DogVille</title>
  </head>
  <body>
    <h2>
      Welcome to the home of Dogs
    </h2>

    <div class="dog-list">
      <div class="dog-pic"><img width="300px" src="images/dog1.jpg" /></div>
      <div class="dog-pic"><img width="300px" src="images/dog2.jpg" /></div>
      <div class="dog-pic"><img width="300px" src="images/dog3.jpg" /></div>
      <div class="dog-pic"><img width="300px" src="images/dog4.jpg" /></div>
    </div>

    <script src="app.js"></script>
  </body>
</html>

In the home page file above, we link to three files: manifest.json which we will use to set up the Add to Home Screen PWA feature, styles.css to apply some basic styling to our page, and app.js which will load in the service worker that we haven’t created yet, but will shortly. In the body of our page, we have a title that reads Welcome to the home of Dogs and below it, we display a list of dog pictures. As you can see, this is a dog site (apologies to the cat lovers).

The dog images are contained in an images folder at the root of the project. I have referenced the dog pictures in index.html according to the filenames used in images. Go ahead and create this folder, you can download dog pictures for free here and rename them appropriately.

Let’s add styles by creating the file styles.css at the root of our project and pasting the following code in it:

body {
  background-color: orange;
}

h2 {
  color: white;
}

This file gives our page background color and makes the title text white. Let’s take our app for a spin by running the following command at the root of the project to invoke the global http-server module to spin up a local server to serve our app:

http-server

You will see a screen similar to the one below when you load the URL in your browser (your dogs will likely be different from mine):

App first view

Adding a service worker

Now, let’s add the juice of PWAs, the service worker. Create a serviceworker.js file at the root of your project and paste the following code in it:

var cacheName = "sw-v1";
var filesToCache = [
  "./",
  "./index.html",
  "./styles.css",
  "./app.js",
  "./images/dog1.jpg",
  "./images/dog2.jpg",
  "./images/dog3.jpg",
  "./images/dog4.jpg"
];

self.addEventListener("install", function (e) {
  console.log("<ServiceWorker> ---- Install v1");
  e.waitUntil(
    caches.open(cacheName).then(function (cache) {
      console.log("<ServiceWorker> --- Caching app shell");
      return cache.addAll(filesToCache);
    })
  );
});

self.addEventListener("activate", (event) => {
  event.waitUntil(self.clients.claim());
});

self.addEventListener("fetch", function (event) {
  event.respondWith(
    caches.match(event.request).then(function (response) {
      if (response) {
        return response;
      }
      return fetch(event.request);
    })
  );
});

In our service worker file above, we cache all our static files including our project root and our images. We then listen for the install event to install our service worker and create a cache for these files using the specified cacheName as the identifier.

Next, we listen to the activate event to ensure that any new service worker that has been installed is the one serving our cache and not an old version.

Finally, we listen to the fetch event to intercept any requests and check whether we already have the requested resource in our cache. If so, we serve the cached version, and if not we make a new request to fetch the resource.

Now, let’s load our service worker file into our application. Create an app.js file at the root of the project and paste the following code in it:


if ("serviceWorker" in navigator) {
  window.addEventListener("load", function () {
    navigator.serviceWorker.register("./serviceworker.js").then(
      function (registration) {
        console.log("Hurray! Service workers with scope: ", registration.scope);
      },
      function (err) {
        console.log("Oops! ServiceWorker registration failed: ", err);
      }
    );
  });
}

Let’s take our service worker for a spin. Make sure that your app is still running, then do a hard reload on the browser tab where the application is currently loaded (Ctrl + Shift + R). Now check the browser console to see the console log messages we wrote to confirm the installation of the service worker. You will see the following messages in your console.

Service Worker Installation

To confirm that we now have offline capabilities with our service worker installed, shut down the http-server service with Ctrl + C, then refresh the application on the browser. You would usually see the offline page at this point because the application is no longer running, but with the magic that is a service worker, you can still see your application home page. Also notice that all dog pictures are loading offline.

Great!

Adding a manifest file

Let’s wrap up our demo application by creating our manifest.json file at the root of the project. You can generate a simple manifest.json file with icons here.

Below is the code in my manifest.json file. Note: I have removed some icons in my example.

{
  "name": "my-dogsite-pwa",
  "short_name": "my-dogsite-pwa",
  "theme_color": "#000000",
  "background_color": "#ffffff",
  "display": "standalone",
  "orientation": "portrait",
  "scope": "/index.html",
  "start_url": "/index.html",
  "icons": [
    {
      "src": "icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

In the above file, the name of the application, the short name , the preferred orientation, the theme_color for the app bar, and the background_color for the splash screen are defined.

Note: It is a best practice to add the short_name, an optional field that specifies the name that will be displayed in the app launcher or a new tab page. Otherwise, the name will be used, and it will be truncated if it is more than 12 characters long.

The icons array is also added with a collection of icon object definitions that point to the icons we will be using for our PWA. Remember to create the icons folder and fill it with the icons named in the manifest file.

Setting up deployment to Firebase

Now that our PWA is complete, let’s begin preparing it for deployment to Firebase. You need to have Firebase tools installed. To check that you have it installed, run the following command:

firebase

This will return a list of Firebase commands to your CLI. If it doesn’t, you need to run the following command to get it installed:

npm install -g firebase-tools

You also need to run the above command if your firebase-tools is less than version 8. To check your firebase-tools version, run the following command:

firebase --version

To set up Firebase hosting for our project, we need to create a Firebase project. Head over to your Firebase console and create a new Firebase project.

Click Add Project and enter the name of your project in the first page that pops up.

Create Firebase Project

Click Continue and on the next page about adding Google Analytics, turn off the Enable Google Analytics for this project toggle button. Since this is a demo project, we won’t be needing analytics.

Now click Create Project. Wait for Firebase to complete setting up your project then click Continue to navigate to your project dashboard.

Now that we have our project set up, the next step is to set up our PWA to be hosted on Firebase using the project we just created. Stay logged into Firebase on your default browser, then go to your CLI to run the following command:

firebase login:ci

This command will log you into Firebase by redirecting to your browser where you’re currently logged in. Once the authentication process is complete, your Firebase token will be printed on the screen just below the line that reads ✔ Success! Use this token to login on a CI server. Save this token securely because you will need it later on in this tutorial.

Next, run the following command at the root of your PWA project to initialize the Firebase setup:

firebase init

The first prompt you will get from this command is

? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices.

. Navigate to Hosting option using the arrow keys, hit Spacebar to select, and hit Enter to go to the next prompt.

Select Project Features

The next prompts is to associate your local project with a Firebase project on your Firebase account. From here, you can choose to use an existing project or create a new one. Select Use an existing project and hit Enter to move to the next prompt. This selection will prompt the CLI tool to load your Firebase projects for you to select from in the next prompt. I am selecting the project we just created on our Firebase console.

Hit Enter to confirm your selection.

Select Project

The next prompt asks for the project folder and suggests the folder public. For our project, everything is taking place at the root, so simply type / and hit Enter to proceed.

The next prompt is ? Configure as a single-page app (rewrite all urls to /index.html)?. As our entire application resides within index.html, type y and hit Enter. This prompt is important for distinguishing between single-page apps and traditional multi-page apps so that Firebase Hosting knows how to handle them.

The next prompt detects that we already have an index.html file and asks if it should be overwritten. Type N for this and hit Enter. This completes the setup and you will now have a .firebaserc file which sets the project id for this application, a firebase.json file which contains details about the options we selected during the set up process and some other default settings, and a standard .gitignore file for Firebase.

With this, we can now proceed to creating our deployment pipeline.

Building the CD pipeline

To set up our automated deployment pipeline, we need to take the following steps:

  1. Push our project to a remote repository (GitHub in this case) connected to our CircleCI account
  2. Add our application as a new project on CircleCI
  3. Add our Firebase token as an environment variable to our CircleCI project
  4. Install firebase-tools locally in the project
  5. Create our pipeline confirguration file
  6. Push project changes to our repository to initiate deployment

Let’s begin. Scaffold a quick package.json file by running the following command:

npm init -y

Then, push the project to GitHub.

The next step is to set up the repository for our project as a CircleCI project.

On the CircleCI console, go to the Add Projects page.

Add Project

Click Set Up Project. This will load the next screen.

Start Building - Config sample

On the setup page, click Start Building. Before the build starts, you get a prompt to either download and use the provided CircleCI configuration file and have it on a separate branch or to set one up manually.

Start Building - Add manually

Select Add Manually to proceed. This will prompt another dialog that checks to confirm that you have a configuration file set up to begin building.

Start Building - Confirm configuration

Click on Start Building to complete the setup. This will immediately trigger the pipeline. The build will fail because we haven’t added our pipeline configuration file.

Our next step is to add our Firebase token as an environment variable in the CircleCI project we just created. On the Pipelines page, with our project selected, click Project Settings.

Project Settings

On the settings page side-menu, click Environment Variables. On the variables set up page, click Add Environment Variable. A dialog box will appear. In the Name* field, enter FIREBASE_TOKEN, and in the Value* field, paste in the Firebase token you got from your CLI in the step above. Click Submit to complete the process. You now have the token variable registered.

Project Settings

Return to the PWA project on your system. Run the following command to install firebase-tools at the root of the project so you can have it registered in package.json as a development dependency:

npm install -D firebase-tools

Once that process is complete, its time to create our deployment configuration file. At the root of your project, create a folder named .circleci, and in it a file named config.yml. Inside the config.yml file, enter the following code:

version: 2
jobs:
  build:
    docker:
      - image: circleci/node:10.16.0
    working_directory: ~/repo
    steps:
      - checkout
      # Download and cache dependencies
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "package.json" }}
            - v1-dependencies-
      - run:
          name: Install Dependencies
          command: npm install
      - save_cache:
          key: v1-npm-deps-{{ checksum "package-lock.json" }}
          paths:
            - ./node_modules
      - run:
          name: Deploy to Firebase
          command: ./node_modules/.bin/firebase deploy --token "$FIREBASE_TOKEN" --only hosting

In the configuration file above, we start by checking out the project from our remote repository. We then install our dependencies, cache them, and run our firebase-tools from the local installation to use our Firebase token to deploy our application.

Now the moment of truth. Let’s commit our changes and push them to our repository to cause our deployment script to be triggered and deploy our application to Firebase hosting.

Build success

Click into the build to see the behind-the-scenes of the project.

Build Process

From the Deploy to Firebase section. You can see the URL of the deployed application. For this exercise, it’s https://my-dog-site-pwa.web.app. Load yours into your browser to test your application.

Live Application

As you can see above, the address bar is loading our Firebase URL, and we can see console messages indicating that our service worker is installed. If you turn off your network and refresh this page, you will see that the application, with all the dog pictures and styling, loads instead of the usual offline screen.

Conclusion

Setting up SSL certificates for https:// URLs is not a task most developers enjoy. Sometimes this leads to reluctance in adopting PWAs. However in this post, we have demonstrated how to set up an automated continuous integration / continuous deployment pipeline for secured hosting of PWAs with CircleCI and Firebase.

Happy Coding!


Fikayo is a fullstack developer and author with over a decade of experience developing web and mobile solutions. He is currently the Software Lead at Tech Specialist Consulting and develops courses for Packt and Udemy. He has a strong passion for teaching and hopes to become a full-time author.