Offline-First with Node.js and Hoodie: A Practical Introduction to Progressive Web Apps
Tags: javascript, webdev, pwa, offlinefirst
Published: 2018-06-11 11:16:47

---
title: Offline-First with Node.js and Hoodie: A Practical Introduction to Progressive Web Apps
published: true
canonical_url: https://www.twilio.com/blog/2018/03/practical-introduction-pwa-node-hoodie-offline-first.html
description: a practical introduction to offline-first & progressive web apps
tags: #javascript, #webdev, #pwa, #offline-first
cover_image: https://www.twilio.com/blog/wp-content/uploads/2018/03/26Dop4gvU_JNU8bnEHaeb-5IFu-fSLzTex6OARsORc5lQm6f95rN307apMDG458d0h3NeFDDnp96CxTqW6Xwk0_vK6Yq8LG_mGp4HbAhIsAmY-1ds8uIQLxpYihQ1cGwK2i4SOPA.png
---
**Progressive Web Apps** (or PWAs for short) aim to deliver a better and engaging user experience by applying progressive enhancements using modern web standards and best practices. These include among others [service workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API), [push APIs](https://developer.mozilla.org/en/docs/Web/API/Push_API), [background sync](background sync) and serving your page over HTTPS.

If the app is loaded in browsers that doesn’t support a web standard, it should work just like a normal website. In modern browsers, however, the experience should be improved by ensuring the app:

- works reliably irrespective of the user network condition (4G, 3G, 2G or offline)
- is installable and feels natural on the user’s device

We’re going to walk through building a shopping tracker PWA which will introduce you to working with Service Workers, the Cache API, and Hoodie. To follow along, you'll need to have [Node.js](https://nodejs.org/en/) and [npm](https://www.npmjs.com/) installed. 


# Development Setup
To set up your environment, clone the files on https://github.com/pmbanugo/shopping-list-starter. Clone and install the project dependencies by running the following commands in your command-line:

```shell
git clone https://github.com/pmbanugo/shopping-list-starter.git
cd shopping-list-starter/
npm install
```

The dependencies installed are Babel and related plug-ins which we’ll use later for transpiling. Babel allows us to write the latest standard in JavaScript and then convert it down to older standards such as ES5 so that our code will run in any of today's browsers. We’re going to use some of ES2015 features such as [let](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let), [const](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/const), [arrow functions](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Arrow_functions) and [ES modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import). The files contained in the **public** directory are the pages and CSS files needed to render a nice looking UI. 

Here’s what you’ll be building towards:

![](https://www.twilio.com/blog/wp-content/uploads/2018/03/81wGimLqxB8U3xtzvB3rJnrrGo39fK3BsQFzodw63_B0TuD3gOzpMR7X5NGn-I07XpxJ3sKGwisV9qXnNO9eEWsMgzLvVlIB43w_-ydgmjvZet8dB5MX8BrN26YFWgTw7WLxoriz.png)


![](https://www.twilio.com/blog/wp-content/uploads/2018/03/1SZ-kxjbuM83S4pmMZ0LzgtUE0diKobqJtEFqGW0KQWnTh5Js0CqRNVmLQFh_XAXuFoxzrH04iTOWl9K0coAbygVoCBLjpdIdrh1nyrJhd9bSuoQwj2BviIYoJYMh39K8JYF_FwQ.png)


The app allows adding items with their prices to the list, save it, and see a summary displayed on a separate page. 

# Saving Data
To add functionality for saving and removing shopping items, we’ll be adding an `npm` package named [Hoodie](http://hood.ie/) to the project. Run the following command to install it (as at the time of this writing, I’m using version 28.2.2 of hoodie)

```shell
npm install --save hoodie
```

# Quick Intro to Hoodie and *Offline-First*
One of the main features of a PWA is the ability to work offline as well as online, hence, we need to apply the concept of *offline-first* to the application. 

**_Offline-First_** is an approach to software development where lack of network connection is not treated as an error.  You start by developing it to work in areas with no internet connection. Then as users enter areas with network connection or as their connection speed improves, the application is progressively enhanced to make more functionality available in the app.  For this tutorial, we want to be able to add and delete data when users are either offline or online. This is where Hoodie will help out. 

Hoodie is a JavaScript Backend for offline-first web applications. It provides a frontend API to allow you to store and manage data and add user authentication. It stores data locally on the device, and when there’s a network connection, syncs data to the server and resolves any data conflicts. It uses [PouchDB](https://pouchdb.com/) on the client, and [CouchDB](https://couchdb.apache.org/) and [hapi](https://hapijs.com/) for the server. We’ll use it both for user authentication as well as storing the shopping items.

# Adding items
The first functionality we’ll be adding allows users to add new items. There’s a file named `index.js` in `public/js/src.`. It contains functions for displaying items saved to Hoodie in the page.

Edit the `index.html` by adding references to `index.js` and the `hoodie` client script before the `</body>` tag on line 197.

```html
<script src="/hoodie/client.js"></script>
<script src="/js/src/index.js"></script>
</body>
```

The Hoodie client script can be accessed from `/hoodie/client.js` when you run the app. By convention, it also serves files within the public folder. Open `index.js` and add the following content in it

```js
function saveNewitem() {
  let name = document.getElementById("new-item-name").value;
  let cost = document.getElementById("new-item-cost").value;
  let quantity = document.getElementById("new-item-quantity").value;
  let subTotal = cost * quantity;



  if (name && cost && quantity) {
    hoodie.store.withIdPrefix("item").add({
      name: name,
      cost: cost,
      quantity: quantity,
      subTotal: subTotal
    });



    document.getElementById("new-item-name").value = "";
    document.getElementById("new-item-cost").value = "";
    document.getElementById("new-item-quantity").value = "";
  } else {
    let snackbarContainer = document.querySelector("#toast");
    snackbarContainer.MaterialSnackbar.showSnackbar({
      message: "All fields are required"
    });
  }
}



function init() {
  hoodie.store.withIdPrefix("item").on("add", addItemToPage);

  document.getElementById("add-item").addEventListener("click", saveNewitem);

  //retrieve items on the current list and display on the page
  hoodie.store
    .withIdPrefix("item")
    .findAll()
    .then(function(items) {
      for (let item of items) {
        addItemToPage(item);
      }
    });
}

init();
```

When this script is loaded in the browser, it calls `init()`  which fetches all items saved locally by calling `hoodie.store.withIdPrefix("item")` and renders them on the page by calling `addItemToPage(item)`  for each item retrieved from the local store. 

We subscribe to the `add` event on the **item** store using `hoodie.store.withIdPrefix("item").on("add", addItemToPage)`. With every new item added to the store, it calls the `addItemToPage` function.  When the Add Item button is clicked on the page, it calls `saveNewItem` to save the data.

# Removing Items
To remove items from the store, you call `hoodie.store.withIdPrefix("item").remove(itemId)` with the ID of the item to remove. 

Modify `index.js` adding the following content before the `init()` call.

```javascript
function deleteRow(deletedItem) {
  let row = document.getElementById(deletedItem._id);
  let totalCost = Number.parseFloat(
    document.getElementById("total-cost").value
  );
  document.getElementById("total-cost").value =
    totalCost - deletedItem.subTotal;
  row.parentNode.removeChild(row);
}



function deleteItem(itemId) {
  hoodie.store.withIdPrefix("item").remove(itemId);
}
```

Alter the `init()` function to include the following lines:

```javascript
function init() {
  hoodie.store.withIdPrefix("item").on("add", addItemToPage);

  hoodie.store.withIdPrefix("item").on("remove", deleteRow);

  document.getElementById("add-item").addEventListener("click", saveNewitem);

  //retrieve items on the current list and display on the page
  hoodie.store
    .withIdPrefix("item")
    .findAll()
    .then(function(items) {
      for (let item of items) {
        addItemToPage(item);
      }
    });

  window.pageEvents = {
    deleteItem: deleteItem
  };
} 
```

We subscribed to the `remove` event which calls a method to remove the item from the list in the page. Additionally we exposed a `deleteItem` function to the page which will be called when the item is removed from the page. On line 189 in `index.html` you’ll find the statement that connects the `onclick` event of the delete button to this method

```html
//Line 189
<td class="mdl-data-table__cell--non-numeric">
  <button class="mdl-button mdl-js-button mdl-button--icon mdl-button--colored"
  onclick="pageEvents.deleteItem('{{item-id}}')">
  <i class="material-icons">remove</i>
  </button>
</td>
```


Now that we’ve got code to add and delete items, let’s run the app to see if it works. Add in the  “scripts” section of your `package.json` the following to create a start command:

```json
"scripts": {
    ...
  "start": "hoodie"
},
```


Run in your command line the command `npm start` to start the server. Open [http://localhost:8080](http://localhost:8080) in a browser and you should see the page loaded ready to use. Give it  a test by adding and removing a few items:

![](https://www.twilio.com/blog/wp-content/uploads/2018/03/WWkwSfo19pxjXhJNFq2igwF4d6jx7TVzH_4XPnh-QST5VY6wgrJDHC7a5ki4kLyAQY_HdEd5ytL4PIUrtc5wq33jR2sNg_df7HFgJ7RQWZzkDDOmujKPVuQ7L7wq8v5NNsOTP6A1.png)

We can see that our list works and data is saved. But this data is only stored locally and not persisted to the server. How do we make it push data to the server? 

With Hoodie, data is only persisted when the user has been authenticated. When users are authenticated, data is saved locally first, then pushed to the server and synchronised across other devices the user is logged onto. Let's add this needed authentication.

#Login and Register Functionality with Hoodie
We already have markup for *login*, *logout*, and *register* as part of the content for `index.html` which you cloned. Check it out if you want to take look at the markup.

Open the file named `shared.js` in `public/js/src` . This file will hold the code for authenticating users using Hoodie. I placed it in a separate file because it’ll be shared with another page we’ll add later. Edit `login` and `register` functions with the following code:

```javascript
    let login = function() {
  let username = document.querySelector("#login-username").value;
  let password = document.querySelector("#login-password").value;


  hoodie.account
    .signIn({
      username: username,
      password: password
    })
    .then(function() {
      showLoggedIn();
      closeLoginDialog();

      let snackbarContainer = document.querySelector("#toast");
      snackbarContainer.MaterialSnackbar.showSnackbar({
        message: "You logged in"
      });
    })
    .catch(function(error) {
      console.log(error);
      document.querySelector("#login-error").innerHTML = error.message;
    });
};



let register = function() {
  let username = document.querySelector("#register-username").value;
  let password = document.querySelector("#register-password").value;
  let options = { username: username, password: password };


  hoodie.account
    .signUp(options)
    .then(function(account) {
      return hoodie.account.signIn(options);
    })
    .then(account => {
      showLoggedIn();
      closeRegisterDialog();
      return account;
    })
    .catch(function(error) {
      console.log(error);
      document.querySelector("#register-error").innerHTML = error.message;
    });
};
```

Add the following functions to handle signout in `shared.js`:


```javascript
let signOut = function() {
  hoodie.account
    .signOut()
    .then(function() {
      showAnonymous();
      let snackbarContainer = document.querySelector("#toast");
      snackbarContainer.MaterialSnackbar.showSnackbar({
        message: "You logged out"
      });
      location.href = location.origin;//trigger a page refresh
    })
    .catch(function() {
      let snackbarContainer = document.querySelector("#toast");
      snackbarContainer.MaterialSnackbar.showSnackbar({
        message: "Could not logout"
      });
    });
};


let updateDOMWithLoginStatus = () => {
  hoodie.account.get("session").then(function(session) {
    if (!session) {
      // user is signed out
      showAnonymous();
    } else if (session.invalid) {
      // user has signed in, but session has expired
      showAnonymous();
    } else {
      // user is signed in
      showLoggedIn();
    }
  });
};

```

Update the export statement to include the two newly added functions:


```javascript
export {
  register,
  login,
  ...
  signOut,
  updateDOMWithLoginStatus
};
```

We defined a `register` function which calls `hoodie.account.signUp()` with a username and password. When it’s successful, it calls `hoodie.account.signIn()` to log the user in. Also we added `login` and `signOut` methods to sign in and sign out, respectively. These APIs for authentication live in `hoodie.account`. The method `updateDOMWithLoginStatus()` updates the navigation bar to display different links based on if the user is authenticated or not. 


Update `index.js` to make use of this file. First add an import statement at the top of the file:

```javascript
import * as shared from "shared.js";
```


Modify the `init` function to call `shared.updateDOMWithLoginStatus()` when the page is loaded in order to update the navigation bar. Then, map the `login` and `signOut` functions to the `pageEvents` object (adding a comma after the deleteItem function):

```javascript
function init() {
  shared.updateDOMWithLoginStatus();
  hoodie.store.withIdPrefix("item").on("add", addItemToPage);
  hoodie.store.withIdPrefix("item").on("remove", deleteRow);



  window.pageEvents = {
    ...
    closeLogin: shared.closeLoginDialog,
    showLogin: shared.showLoginDialog,
    closeRegister: shared.closeRegisterDialog,
    showRegister: shared.showRegisterDialog,
    login: shared.login,
    register: shared.register,
    signout: shared.signOut
  };
}
```

We’ve used ES modules here. We've been using ES2015 modules in our code, however, not all browsers support this yet, so we need a way to make this work for all. We’ll use Babel to transpile the code to work with [SystemJS](https://github.com/systemjs/systemjs), a module loader enabling dynamic ES module workflows in browsers and Node.js. (We already have the needed files to do this.) 

# Transpiling Our Code for Increased Browser Support
The GitHub repo you cloned already has a `system.js` file in `public/resources/system.js`. We also installed Babel as part of the dependencies (see `package.json`), and a Babel configuration file (see `.babelrc`). 

```json
//file -> .babelrc
{ 
    "plugins": ["transform-es2015-modules-systemjs"],
    "presets": ["es2015"] 
}
```

This tells Babel to transpile our JavaScript code to ES5 compatible code, and convert any ES2015 module into SystemJS module.

To trigger the transpiling, we are going to add a build script in `package.json` as follows:

```json
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "hoodie",
    "build": "babel public/js/src --out-dir public/js/transpiled"
  }
```

Edit `index.html` to include a reference to SystemJS and the transpiled `index.js` below our include for `hoodie`:

```
<body>
....
  <script src="/hoodie/client.js"></script>
  <script src="resources/system.js"></script>
  <script>
    System.config({ "baseURL": "js/transpiled" });
    System.import("index.js");
  </script>
</body>
```


Now, run the following command to transpile the code

```shell
npm run build
```

Relaunch the server if necessary (`npm start`), refresh the page, then try to register, signin, and signout

![](https://www.twilio.com/blog/wp-content/uploads/2018/03/MFgw39sCu5LemsduhV9-6kcoTpiJQyfZ2LSahDt2VnmLgZYv3X7mIwEocSbFUXo5G6NhFORSZ4_ZD6JESCnpFrOsXPvnyFjXVk8JZD5u08B6xfhkj_sKfaxHT0zsESv7LtEnI2L4.png)

# Saving the Shopping List and viewing History
After adding and removing items as you like, you’ll probably want to save the list of items and get a summary of the totals on a separate page. In the completed GitHub repo, I have a function [saveList()](https://github.com/pmbanugo/shopping-list/blob/d660f5e25fe55c45b720eb22a61df47dd8b31897/public/js/src/index.js#L63)  in index.js. This method:

1. Gets all the saved items from the item store by calling `hoodie.store.withIdPrefix("item").findAll()`, 
2. Calculates the total of all the items
3. Saves the total cost of the items together with the items in the list store (`hoodie.store.withIdPrefix("list").add({cost, items})` ) 
4. Removes all items from the item store so new ones can be added. 

We’ll summarize lists with the price and the date it was added on a different page, `history.html`. The script to handle this is in the file `history.js` in `public/js/src/` of the completed code on GitHub. I have omitted showing this code here for the sake of brevity. The code is similar to what we’ve written up until this point. 

If you copied over the code from those sources onto your working directory, run the build script again (`npm run build`) and refresh the page. If that worked, add a few items and save the list. When you go to the history page, you should see the saved list there: 

![](https://www.twilio.com/blog/wp-content/uploads/2018/03/26Dop4gvU_JNU8bnEHaeb-5IFu-fSLzTex6OARsORc5lQm6f95rN307apMDG458d0h3NeFDDnp96CxTqW6Xwk0_vK6Yq8LG_mGp4HbAhIsAmY-1ds8uIQLxpYihQ1cGwK2i4SOPA.png)

Nice work, it’s really coming together! Now let’s discuss adapting our application for seamless offline usage.

# Offline Page Loading
So far we’ve been able to save and view data. This works when the user is offline and even when the server is down, then it’ll sync to the server when there’s a connection. 

However, at the moment we’re going to see an error when we try to load the page while being offline. Let’s fix that by utilizing Service Workers and the Cache API.

## A quick Intro to Service Workers and the Cache API
A [Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) is a programmable network proxy, which runs on a separate browser thread and allows you to intercept network requests and process them as you so choose. You can intercept and cache a response from the server and the next time the app makes a request for that resource, you can send the cached version. It runs regardless of whether the page is currently open or not.

We’re going to add a Service Worker script which will intercept all network request and respond with a cached version if the resource refers to our page and its related assets. This resources will be cached using the Cache API.

The Cache API, which is part of the Service Worker specification, enables Service Workers to cache network requests so that they can provide appropriate responses even while offline.  

## Create a service worker script
Add a new file named `sw.js` in the public folder at `public/sw.js`. To tell the browser that we want this script to be our service worker script, open `shared.js` and add this code to the top of your file:

```js
if ("serviceWorker" in navigator) {
  navigator.serviceWorker
    .register("sw.js")
    .then(console.log)
    .catch(console.error);
}
```


This code will first check if the browser supports service workers, and then register the file `sw.js` as the service worker script. 

Run the build script again (`npm run build`) and refresh the page. 

If you haven’t yet, open your browser JavaScript console (here is how to do it in [Chrome](https://developers.google.com/web/tools/chrome-devtools/console/?utm_source=dcc&utm_medium=redirect&utm_campaign=2016q3), [Firefox](https://developer.mozilla.org/en-US/docs/Tools/Browser_Console) and [Edge](https://docs.microsoft.com/en-us/microsoft-edge/devtools-guide/console)), you should see something printed to the console regarding service workers. Navigate to the Application tab (or similar if you are not in Chrome) in your dev tools and click on “Service Workers” from the side menu, you should see something similar to this screen:

![](https://www.twilio.com/blog/wp-content/uploads/2018/03/dMunIdK88bKlfquLZFr5aE3jOx4mLDQSFFF5nhN5YnhOYaf9_tJUSvlYfmSs7C3wY5UseMQoP4ybbeAiMjwG2Xfk90kOGgxXuadIC6RIYjAUp4GZrzvbzr2NqjcOMGaQTSyWfxQF.png)

Registering a service worker will cause the browser to start the service worker `install` step in the background. It is at this install step that we want to fetch and cache our asset. 

If the asset is successfully cached, then it is installed and move to the **_activate step_**. If it failed, the service worker will not be installed. The activate step is where we need to delete old caches of our assets so our service worker can serve updated resources. 

After the activation step, the service worker will control all pages that fall under its scope. The page that originally registered the service worker for the first time won't be controlled until it's loaded again. 

All these steps (install and activate) that happen after registration are part of the *life cycle* of a service worker. You can read more about these concepts [later](https://developers.google.com/web/fundamentals/primers/service-workers/#the_service_worker_life_cycle). 

## Modifying Our Service Worker
Our Service Worker script is currently empty. For us to listen for the install step and cache all our assets using the Cache API, add the following code in `sw.js`:

```javascript
const CACHE_NAME = "cache-v1";
const assetToCache = [
  "/index.html",
  "/",
  "/history.html",
  "/resources/mdl/material.indigo-pink.min.css",
  "/resources/mdl/material.min.js",
  "/resources/mdl/MaterialIcons-Regular.woff2",
  "/resources/mdl/material-icons.css",
  "/css/style.css",
  "/resources/dialog-polyfill/dialog-polyfill.js",
  "/resources/dialog-polyfill/dialog-polyfill.css",
  "/resources/system.js",
  "/js/transpiled/index.js",
  "/js/transpiled/history.js",
  "/js/transpiled/shared.js",
  "/hoodie/client.js"
];
self.addEventListener("install", function(event) {
  console.log("installing");
  event.waitUntil(
    caches
      .open(CACHE_NAME)
      .then((cache) => {
        return cache.addAll(assetToCache);
      })
      .catch(console.error)
  );
});
```

We call `caches.open(CACHE_NAME)` which opens or creates a cache and returns a Promise with cache object. Once we have that object, we call `cache.addAll()` with an array of all the things we want to cache to make the app load while being offline. 

The call is wrapped in `events.waitUntil` which tells the browser not to terminate the service worker until the Promise passed to it is either resolved or rejected. A Service Worker can be terminated by the browser after a while of being idle and we need to prevent that from occuring before we are done with our caching. 

Refresh the page and it will trigger the registration of the Service Worker. Open the Applications tab in DevTools if you use Chrome (or the developer tool in your preferred development browser), click to open the ‘Cache’ menu and you should find a cache with the name we used. Click on it and you’ll see the files listed there

![](https://www.twilio.com/blog/wp-content/uploads/2018/03/xIvEH5AVhrOC_FgsvVdIfe7odplyDs818gbjPwwAP88rRVaXw3Vh2aSxHPWy7b1HzULylAvVWAep2uppi-gpKlt2hJHQxFXoRZFDN6cTyrJIOyP3sIPvHNHEW1kg1Rnqe8xHAI14.png)

We’ve added our assets to the cache, but we need to serve the browser our cached asset each time it makes a request for one. To do this, we listen to the `fetch` event which is called each time the browser is about to make a request. 

Add the following code in `sw.js` to intercept all network request and respond with a cached response if it’s a request for any of our cached assets:

```js
self.addEventListener("fetch", function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      if (response) {
        return response; //return the matching entry found
      }
      return fetch(event.request);
    })
  );
});
```

We’ve used `event.respondWith()`, a method of [FetchEvent](https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith). It prevents the browsers default handling of the request and returns a promise of a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object. Our implementation either returns the response from the cache if it’s available, or makes a formal request using the Fetch API and returns whatever we get from that response. 

Save `sw.js`,open and refresh the page once again to re-install the service worker. You’ll probably notice that the service worker stops at the activate step:

![](https://www.twilio.com/blog/wp-content/uploads/2018/03/PLzu84mpBQuT-OnA7tVY-ua4DHlbDpyeAMWYQFuu7kGuF0EPenq3jGEX8_44yK8tyXSAY4ZZ03Z2MB3aTqqAGpo0Z1MH8h39SVa2hTJDHsJL2xJijJWmjFGBa6Cr_LM1g0hJkssJ.png)

Click the skip waiting link to immediately activate it.

Refresh the page to see that the assets are loaded from the service worker
in the ‘Network’ tab:

![](https://www.twilio.com/blog/wp-content/uploads/2018/03/CHlTkcvGpasgSzvFI8DFeQq5EvnM1soO19lB_upAjroU4zaO7_pktIxAFvkIOU_6BZCxcz3ZfrqYX8u40-XKAMCV4RCTCOUNUkeKvSTFx_N08bNJ65QZJPhbiRSLyh5EBDQVsGMs.png)

You can then select the offline network throttling option in dev tools, refresh the page and navigate around to see that it works:

![](https://www.twilio.com/blog/wp-content/uploads/2018/03/WH50vyewOHRfvj9udO-BcfpxqGX1hc_G_ZA6Lb5jeP2HohUXNBcUV7h-OLopeP100FWUR2pImFiVGk_Egjoco_V1EuxvpjTcbQmP6sEIFe2ADWtGtbhQUKhSeaFJWj8-59Kr8vXS.png)

# That’s a wrap!
We built a basic shopping tracker application that works offline. Offline First is a core part of progressive web applications and we’ve tackled that with Hoodie and Service Workers. Hoodie provides the backend to easily build an offline-first app that synchronises the data among the server and all connected devices. Service Workers allow us intercept requests and respond with cached results. For browsers that do not support service workers we will gracefully fall back to working like a normal website.

We don’t quite have a complete PWA yet, however, we’ve just laid the foundation. In another post, we'll look at some other bits you need to make it a PWA and some helpful tools to generate what you need (including using workbox to generate a service worker script). Stay tuned!

You can find complete source code of the final application on [GitHub](https://github.com/pmbanugo/shopping-list). If you’re confused about any of the things I talked about in this post, please leave a comment and feel free to reach out to me on Twitter (I’m happy to connect and see what you build next 🚀). 

> Peter Mbanugo is interested in offline-first and constantly seeking to learn better ways to build fast, light and performant web apps. His current project is a [real-time app state synchronisation service](hamoni.tech/). You can invite him to come speak at your event, write for you, craft software, or discuss Offline-First, and Software Architecture. Reach him anytime on twitter [@p_mbanugo](https://twitter.com/p_mbanugo)

# Reference
- [Offline First community](http://offlinefirst.org/)
- [Hoodie](http://hood.ie/)
- [Service Worker](https://developers.google.com/web/fundamentals/primers/service-workers)
- [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api)