Offline-first made easy with GraphQL, Amplify DataStore and Vue
Tags: aws, amplify, vue, javascript
Published: 2020-10-01 11:10:08

Create a PWA cloud-enabled offline-first chatroom using Vue

![Image for post](https://miro.medium.com/max/2624/1*Y6p-Vy2Gkh69BtCrfgOP8g.png)

In this article, you will create Chatty, a chatroom Progressive Web App (PWA) that stores data on the device while offline, and synchronises in real-time with other devices when online using Amplify DataStore, GraphQL and Amazon DynamoDB. We will cover:

*   Introduction to **Offline-first**, **PWAs** and **Amplify DataStore**
*   Setting up a new project with the **Vue CLI**
*   Creating a new **GraphQL API**
*   **Amplify DataStore**: setup, data models and usage
*   **Creating the UI with Vue**: the chatroom, sending and deleting messages; and doing real-time with subscriptions
*   Making Chatty a **PWA**
*   Adding a **PWA custom configuration**
*   **Improving UX** while offline
*   Publishing your app via the **AWS Amplify Console**
*   Installing Chatty in the **Desktop and Mobile**
*   Cleaning up **cloud services**

> _In order to follow this post you will need a basic knowledge of GraphQL. You can learn the basics following this tutorial at_ [_graphql.org_](https://graphql.org/learn/)_. We will be referring to GraphQL schema, directives, types, queries, mutations, subscriptions and resolvers._

Please let me know if you have any questions or want to learn more at [@gerardsans](https://twitter.com/intent/user?screen_name=gerardsans).

\> [Final solution](https://github.com/gsans/amplify-datastore-chatty-pwa-vue) and step by step tutorial in [GitHub](https://github.com/gsans/amplify-datastore-chatty-vue).

Introduction to Offline-first, PWAs and Amplify DataStore
=========================================================

Offline-first is an approach to software development, where an application is built to function, with or without an internet connection. Using this approach, data is stored locally on the user’s device and periodically uploaded and replicated into the cloud. Features include:

*   **Offline ready**: application works offline. This usually comes with some limitations regarding feature set and device capabilities. Eg: PWAs require an install step where the user needs to be online to download all the necessary application assets.
*   **Great user experience**: application loads fast and transitions seamlessly from online to offline improving user retention. The user is in control regarding app updates, data synchronisation, data conflict resolution and connectivity. Eg: App shell; user is informed about connectivity changes.
*   **Native-like features**: application behaves similar to a native app. Eg: PWAs are able to run on multiple operating systems as standalone applications and don’t require the user to interact with the browser.
*   **Reliable storage**: user can exit the application at any moment without losing data. Application stores user data securely on-device and synchronises with the cloud transparently when possible.

Progressive Web Apps
--------------------

Progressive Web Apps combine the benefits from native apps, while being able to run on more platforms, leveraging browser technologies. PWA features allow Web apps to close the gap with native applications while creating similar user experiences. PWA features:

*   Works **offline** via service worker.
*   **Great** **performance** via app shell and pre-cached assets.
*   Access to **device APIs** via [Web APIs](https://whatwebcando.today/).
*   Supports **push notifications** via service worker.
*   Works in **Web, Mobile and Desktop** via Web app manifest.

The most important requirements for PWA are: security, must be served via HTTPS, the [service worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers) and the [Web app manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest). We will incorporate all of these to Chatty.

Amplify DataStore
-----------------

Amplify DataStore, is an on device persistent repository for interacting with local data, and able to automatically synchronise via GraphQL. Using Amplify DataStore, you can provide a **great offline-first experience** for your users, while using a **simple programming model**.

> GraphQL knowledge is beneficial when using Amplify DataStore to understand its inner workings but not necessary to use DataStore APIs.

Amplify takes care of all the heavy lifting regarding hosting your data source in the cloud using **Amazon DynamoDB**; and scaling real-time messaging, synchronising data and running conflict resolution with **AWS AppSync** and **GraphQL.**

Amplify DataStore uses your **GraphQL schema** to generate data models. These will help keep your client code consistent with the type and fields defined in your GraphQL schema; and reduce type checks and data related run-time errors while enforcing **GraphQL type safety**.

Amplify DataStore behaves differently when in offline or online modes:

*   **In offline mode,** we use the **DataStore API** to manage the data on the device via the **Storage Engine** using GraphQL. The local **Data Store** being used will vary depending on the user device. This can either be IndexedDB in the browser or SQL Lite in Android and iOS.

![Image for post](https://miro.medium.com/max/2792/1*tfo7SD-FVUjTqqnvjUWQgQ.png)
*Amplify DataStore data flow in offline mode*


*   **In online mode,** Amplify DataStore, will synchronise with the cloud using the **Sync Engine** via GraphQL to automatically synchronise with your GraphQL API. At the query level, this is implemented as an **AWS AppSync resolver** accessing **Amazon DynamoDB**, **AWS Lambda** or **Amazon ElasticSearch**.

![Image for post](https://miro.medium.com/max/2808/1*5Ts59jaZtvpwKyBorewthA.png)
*Amplify DataStore data flow in online mode*

Amplify DataStore supports the following conflict resolution modes during synchronisation:

*   **Optimistic concurrency**, this mode rejects any incoming changes if there’s a mismatch in the tracking version between the incoming change and the item being changed. A further step on the client is required to resolve the conflict.
*   **Auto merge** (default), this mode doesn’t require further steps to resolve conflicts. It accommodates mutations on a field-by-field basis as long as scalar types don’t clash. If they do, they are ignored but all other fields are merged, if possible.
*   **Custom**, this mode uses AWS Lambda to allow more control and complex use cases.

For more details read [conflict detection and sync](https://docs.aws.amazon.com/appsync/latest/devguide/conflict-detection-and-sync.html).

Setting up a new project with the Vue CLI
=========================================

Before moving to the next section, please complete the steps described in “[Build your first full-stack serverless app with Vue](/@gerard.sans/build-your-first-full-stack-serverless-app-with-vue-and-aws-amplify-9ed7ef9e9926)”. Here you will set up the initial project, familiarise with **Amplify CLI** and add an authorisation flow so users can register themselves via an automated verification code sent to their email and login.

Creating a new GraphQL API
==========================

First, we are going to create the GraphQL API to service **Chatty** chatroom. To create it, we will use the following command:

```
amplify add api
```

Answer the following questions

*   Please select from one of the below mentioned services **GraphQL**
*   Provide API name: **ChattyAPI**
*   Choose the default authorization type for the API **API key**
*   Enter a description for the API key: **(empty)**
*   After how many days from now the API key should expire (1–365): **7**

> It’s important that you don’t miss the following steps to set up conflict detection. If you missed it, you can recover with `amplify api update`

*   Do you want to configure advanced settings for the GraphQL API **Yes, I want to make some additional changes.**
*   Configure additional auth types? **No**
*   Configure conflict detection? **Yes**
*   Select the default resolution strategy **Auto Merge**
*   Do you want to override default per model settings? **No**
*   Do you have an annotated GraphQL schema? **No**
*   Choose a schema template: **Single object with fields (e.g., “Todo” with ID, name, description)**
*   Do you want to edit the schema now? **Yes**

When prompted, replace the default schema with the following:

```javascript
type Chatty @model {  
  id: ID!  
  user: String!  
  message: String!  
  createdAt: AWSDateTime  
}
```

By adding `@model` to the `Chatty` type we are telling the **Amplify CLI** to create the resolvers to support queries, mutations and subscriptions in **Amazon DynamoDB**. Besides the regular **CRUD operations**, we are also getting some more advanced features like pagination, filtering and real-time synchronisation that we are going to use later.

Pushing your GraphQL API to the cloud
-------------------------------------

Let’s run the push command to create the GraphQL API:

```
amplify push
```

*   Are you sure you want to continue? **Yes**
*   Do you want to generate code for your newly created GraphQL API **Yes**
*   Choose the code generation language target **javascript**
*   Enter the file name pattern of graphql queries, mutations and subscriptions **src/graphql/\*\*/\*.js**
*   Do you want to generate/update all possible GraphQL operations — queries, mutations and subscriptions **Yes**
*   Enter maximum statement depth \[increase from default if your schema is deeply nested\] **2**

Write down your **GraphQL endpoint** and **API KEY**.

This is the resulting architecture with our new GraphQL API managed by **AWS AppSync** and **Amazon DynamoDB**:

![Image for post](https://miro.medium.com/max/2424/1*aNf0mRhG8vfnEmHl2fYq5g.png)

Run the command below to access the **AWS AppSync console**.

```
amplify console api
```

*   Please select from one of the below mentioned services **GraphQL**

Amplify DataStore setup
=======================

Run the command below to install the necessary dependencies:

```
npm install --save @aws-amplify/core @aws-amplify/datastore
```

Data model generation
---------------------

Generate the data models to manage our messages for our **ChattyAPI**.

```
amplify codegen models
```

> _Important: DO NOT forget to regenerate your models every time you introduce a change in your schema._

After running this command, **AWS Amplify CLI** has generated the necessary data models in a new folder. The files in this folder hold your data model classes and schema.

```
<amplify-app>  
    |\_ src  
      |\_ models
```

Creating a message
------------------

Now that the GraphQL API and **data models** are created, we can begin using the **DataStore API**. The first thing we’ll do is create a new message passing the generated **data model** to `save`.

> Using data models allow us to be always compliant with the GraphQL schema and prevent errors right in the client.

```javascript
import { DataStore } from "@aws-amplify/datastore";  
import { Chatty } from "./models";

await DataStore.save(new Chatty({  
  user: "amplify-user",  
  message: "Hi everyone!",  
  createdAt: new Date().toISOString()  
}))
```

This will create a record locally in your device and synchronise it in the background using the underlying **GraphQL API** when in online mode.

Querying data
-------------

Let’s see how we can query the data using Amplify DataStore. In order to query our **data model** we will use a query and a predicate to indicate that we want all records.

```javascript
import { DataStore, Predicates } from "@aws-amplify/datastore";  
import { Chatty } from "./models";

const messages = await DataStore.query(Chatty, Predicates.ALL);
```

This will return an array of messages that we can display in our UI. **Predicates** also support filters for common types like `Strings`, `Numbers` and `Lists`.

> _Find all supported filters at_ [_query with Predicates_](https://aws-amplify.github.io/docs/js/datastore#query-with-predicates)_._

Creating the UI with Vue
========================

Now, let’s look at how we can create the UI to create and display messages for Chatty chatroom.

```javascript
<template>  
  <div v-for="message of sorted" :key="message.id">  
    <div>{{ message.user }} - {{ moment(message.createdAt).format('YYYY-MM-DD HH:mm:ss')}})</div>  
    <div>{{ message.message }}</div>  
  </div>  
</template>  
<script>  
import { DataStore, Predicates } from "@aws-amplify/datastore";  
import { Chatty } from "./models";  
import moment from "moment";

export default {  
  name: 'app',  
  data() {  
    return {  
      user: {},  
      messages: [],  
    }  
  },  
  computed: {  
    sorted() {  
      return [...this.messages].sort((a, b) => -a.createdAt.localeCompare(b.createdAt));  
    }  
  },  
  created() {  
    // authentication state managament  
    onAuthUIStateChange((state, user) => {  
      // set current user and load data after login  
      switch (state) {  
        case AuthState.SignedIn: {  
          this.user = user;  
          this.loadMessages();  
          break;  
        }  
      }  
    });  
  },  
  methods: {  
    moment: () => moment(),  
    loadMessages() {  
      DataStore.query(Chatty, Predicates.ALL).then(messages => {  
        this.messages = messages;  
      });  
    },  
  }  
}  
</script>
```

After the user signs in, we call `loadMessages`. This fetches all the messages in our local store. We are sorting the list with a computed method `sorted` as we want our messages to show the most recent at the top. We are also using `moment` to format our time variables.

Creating a message
------------------

Now, let’s look at how we create new messages.

```javascript
<template>  
  <form v-on:submit.prevent>  
    <input v-model="form.message" placeholder="Enter your message..." />  
    <button @click="sendMessage">Send</button>  
  </form>  
</template>  
<script>  
export default {  
  data() {  
    return {  
      form: {},  
    };  
  },   
  methods: {  
    sendMessage() {  
      const { message } = this.form  
      if (!message) return;  
      DataStore.save(new Chatty({  
        user: this.user.username,  
        message: message,  
        createdAt: new Date().toISOString()  
      })).then(() => {  
        this.form = { message: '' };  
        this.loadMessages();  
      }).catch(e => {  
        console.log('error creating message...', e);  
      });  
    },  
  }  
}  
</script>
```

We use a form to read the message from the user and call `sendMessage` with `save` passing the user and current date along with the message. Once the message has been created we reset the input field and reload all messages. Note how we use Amplify DataStore as our source of truth in this implementation.

Deleting all messages
---------------------

One of the main advantages of working using Amplify DataStore is being able to run a series of GraphQL mutations without having to use a series of individual operations. See below how we can use delete together with a predicate to remove all messages.

```javascript
DataStore.delete(Chatty, Predicates.ALL).then(() => {  
  console.log('messages deleted!');  
});
```

> When using Amplify DataStore records are never removed, but marked for removal. Records will then be automatically removed as defined in their [TTL](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html) attribute. At the time of writing, this is set to 30 days.

Real-time with GraphQL subscriptions
------------------------------------

Next, let’s see how we can create a GraphQL subscription. To do so, we will listen for changes of data in our API, and update the state whenever a new piece of data comes through. When the component is destroyed, we will `unsubscribe` to avoid memory leaks.

> As we are using Amplify DataStore, as our source of truth, we don’t need another copy of it at the app level so we just need to load the latest state with `loadMessages`.

```javascript
<script>  
export default {  
  data() {  
    return {  
      subscription: undefined;  
    };  
  },  
  created() {  
    //Subscribe to changes  
    this.subscription = DataStore.observe(Chatty).subscribe(msg => {  
      console.log(msg.model, msg.opType, msg.element);  
      this.loadMessages();  
    });  
  },   
  destroyed() {  
    if (!this.subscription) return;  
    this.subscription.unsubscribe();  
  },  
}  
</script>
```

At this point, the chatroom is fully working including authentication flow and data synchronisation in real-time.

![Image for post](https://miro.medium.com/max/2904/1*-OHzDfpbijDHrJG3lWtWnA.png)
*Real-time synchronisation using Chrome and Firefox clients side-by-side.*

On the other hand, we haven’t made much progress in making it work offline or having native-like features. In order to tick these boxes, we are going to make our app a **PWA**.

Making Chatty a PWA
===================

The [PWA CLI plugin](https://cli.vuejs.org/core-plugins/pwa.html#vue-cli-plugin-pwa) can help us making Chatty a PWA by running a single command:

```
vue add @vue/pwa
```

This will make few changes to the project and add a few new assets necessary to run the app as a PWA. The most important change happens in `src/main.js` where we are now registering a [**service worker**](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers) that will allow Chatty to work offline.

Using the default settings, the [**service worker**](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers) is configured to pre-cache all the assets resulting from the build (`js`, `css` and `public` folder). Using this setup, the browser can access the cache to load the app while offline. See the diagram below:

![Image for post](https://miro.medium.com/max/2412/1*5jxEUoFpn8iE-vPAD0-YbA.png)
*Service worker serving assets from the cache while offline*

> Learn more about service workers at [using service workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers).

Run the build command to see changes:

```
yarn build
```

In your `dist` folder you should find the following:

```
└── dist  
  ├── css  
  │   └── app.<version>.css  
  ├── img/icons  
  │   ├── android-chrome-<size>.png  
  │   └── ...  
  ├── js  
  │   ├── app.<version>.png  
  │   └── ...  
  ├── favicon.ico  
  ├── index.html  
  ├── manifest.json  
  ├── precache-manifest.<version>.json  
  ├── robots.txt  
  └── service-worker.js
```

The `index.html` has been modified to include the [Web app manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) `manifest.json` along with some meta tags and entries to support different devices and platforms. You can further customise these settings. The most important is`manifest.json/display` set to `standalone`. This setting will give the app a look and feel like a native application for Desktop and Mobile. Learn about all the options available in [Web app manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest).

Another important file is `service-worker.js`. This contains the implementation for the [**service worker**](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers) which determines the behaviour of the app while offline. As part of it, `precache-manifest.<version>.json` includes all files to be installed and pre-cached as part of the app. This will make the app load almost instantly as it won’t use the network. These files were created by the [PWA CLI plugin](https://cli.vuejs.org/core-plugins/pwa.html#vue-cli-plugin-pwa) and include all the assets from your build.

Test the app running the following commands on a new terminal window:

```
cd dist  
python -m SimpleHTTPServer 8887  // open localhost:8887
```

> Remember that in order to test the app you need to install the service worker first. This requires at least loading the app once.

To test the app while offline you can use the **Developer Tools** or switch off your **internet connection**. See below a screen capture after refreshing the page while offline:

![Image for post](https://miro.medium.com/max/2824/1*2W85dgMo77Tvr4phioyTeA.png)
*Chatty app while offline.*

As you can see in the screenshot above using Chrome, in the **Network tab**, the files are served by the [**service worker**](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers) while offline (in green). Unfortunately the default setup won’t include any custom assets like our **SVG logo** in the header (in red) or external fonts.

Adding a PWA custom configuration
=================================

Let’s fix the issue with our logo by using a **custom configuration**. Create `vue.config.js` with the following content at the root of your project:

```javascript
// vue.config.js  
const manifest = require('./public/manifest.json')module.exports = {  
  pwa: {  
    name: manifest.short_name,  
    themeColor: manifest.theme_color,  
    msTileColor: manifest.background_color,  
    appleMobileWebAppCapable: 'yes',  
    appleMobileWebAppStatusBarStyle: 'black',  
    workboxPluginMode: 'InjectManifest',  
    workboxOptions: {  
      swSrc: 'src/service-worker.js',  
    }  
  }  
}
```

This file controls the [PWA CLI plugin](https://cli.vuejs.org/core-plugins/pwa.html#vue-cli-plugin-pwa) and deal with settings in the [Web app manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) and UX for different devices.

The custom configuration uses [workbox](https://developers.google.com/web/tools/workbox) in `injectManifest` mode and points to the [service worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers) implementation we want to use. Find all configuration options at the [official website](https://cli.vuejs.org/core-plugins/pwa.html#configuration). Now create `src/service-worker.js` with the following code

```javascript
// src/service-worker.js  
workbox.core.setCacheNameDetails({ prefix: 'amplify-datastore' })

workbox.core.skipWaiting()  
workbox.core.clientsClaim()

const cacheFiles = [{  
 "revision": "e653ab4d124bf16b5232",  
 "url": "[https://aws-amplify.github.io/img/amplify.svg](https://aws-amplify.github.io/img/amplify.svg)"  
}]

self.__precacheManifest = cacheFiles.concat(self.__precacheManifest || [])  
workbox.precaching.precacheAndRoute(self.__precacheManifest, {})
```

For our [**service worker**](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers), we are setting a prefix for our cache to easily find our files while using the **Developer Tools**. We are also setting the behaviour for new service worker versions. These will be applied immediately replacing any existing ones with `skipWaiting` and `clientsClaim` as explained [here](https://developers.google.com/web/tools/workbox/modules/workbox-core#skip_waiting_and_clients_claim). To fix our **SVG issue**, we are adding the url to the SVG file, to the **pre-cached files** defined in the [Web app manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest).

Improving UX while offline
==========================

We are almost done. Our app can survive offline reloads, can store messages while offline and can synchronise when back online thanks to Amplify DataStore.

The only remaining issue now is **improving the user experience** as the user doesn’t know when the app is operating online or offline. This is not a great UX, as sometimes **the user might inadvertently go offline** and be confused as for not receiving any feedback or explanation from the app. We can fix this by using the code below:

```javascript
// <div v-if="offline">You are offline.</div>  
// <div v-bind:class="{ offline: offline }">

// App.vue  
import { Hub } from 'aws-amplify';

export default {  
  data() {  
    return { offline: undefined };  
  },  
  created() {  
    this.listener = Hub.listen('datastore', {payload:{event}} => {  
      if (event === 'networkStatus') {  
        this.offline = !data.active;  
      }  
    })  
  }  
}
```

We are using the [**Amplify Hub**](https://docs.amplify.aws/lib/utilities/hub/q/platform/js) to detect **network changes** and use it to set the `offline` flag. Once in place, we can use it to display messages or style the UI accordingly as shown at the beginning of the code above. We are now ready to publish **Chatty** to the cloud and test it using different devices.

![Image for post](https://miro.medium.com/max/944/1*eIii-RnmoTL486dsT1dzNA.png)
*Chatty PWA informing the user of changes in the network status.*

Publishing your app via the AWS Amplify Console
===============================================

The first thing you need to do is **create a new repo** for this project. Once you’ve created the repo, copy the **URL** for the project to the clipboard and initialise git in your local project:

```
git init  
git remote add origin [repo@repoofyourchoice.com](mailto:repo@repoofyourchoice.com):username/project-name.git  
git add .git commit -m 'initial commit'git push origin master
```

Next visit the [**AWS Amplify Console**](https://aws.amazon.com/amplify/console/) in your **AWS account**. Click **Get Started** to create a new deployment. Next, authorise your repository provider as the repository service. Next, choose the new repository and branch for the project you just created and click **Next**. In the next screen, create a new role and use this role to allow the **AWS Amplify Console** to deploy these resources and click **Next**. Finally, click **Save** and **Deploy** to deploy your application!

![Image for post](https://miro.medium.com/max/2048/0*DT15Us8PZ13wdHD2.png)
*AWS Amplify Console deployment steps.*

This will generate a public url that uses **HTTPS**, which is a requirement for **PWAs.** Congratulations! You can now test it in your browser, Desktop and Mobile.

Installing the Chatty app in the Desktop and Mobile
===================================================

Due to the inclusion of the [Web app manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) in our `index.html` page, you can now install the **Chatty** app in the Desktop and Mobile devices in Windows, Mac and Linux. The way it works, changes slightly depending the device and operating system you are using. Try it out following the steps in [Add to Home screen](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Add_to_home_screen).

![Image for post](https://miro.medium.com/max/1956/1*whmy0GeYOcL_31-R1-EEZg.png)
*Use add to Home screen to install in Desktop and Mobile.*

Cleaning up cloud services
==========================

If at any time, you would like to delete a service from your project and your **AWS Account**, you can do this by running:

```
amplify delete
```

Conclusion
==========

Congratulations! You successfully built your first **offline-first app** using **Vue** and **Amplify**. You created a chatroom **PWA** that stores data on the device while offline and when online synchronises in real-time with other devices using **Amplify DataStore, AWS AppSync**, **GraphQL** and **Amazon DynamoDB**. Thanks for following this tutorial.

If you prefer, you can follow the instructions in this video to build the chatroom app.

{% youtube CXeRQn62Ptw %}

Thanks for reading!
===================

Have you got any questions regarding this tutorial or **AWS Amplify**? Feel free to reach out to me anytime at [@gerardsans](https://twitter.com/intent/user?screen_name=gerardsans).

![Image for post](https://miro.medium.com/max/720/1*J9DNmPDsRts_0lMUliXwBw.jpeg)

> My Name is [Gerard Sans](https://twitter.com/gerardsans). I am a Developer Advocate at [AWS Mobile](https://aws.amazon.com/products/mobile) working with [AWS Amplify](https://aws-amplify.github.io/) and [AWS AppSync](https://aws.amazon.com/appsync/) teams.
> GraphQL is an open source data query and manipulation language for APIs, and a runtime for fulfilling queries with existing data.