Introduction

Imagine a web application used simultaneously by many users at the same time. It can become necessary to keep them in sync. We want to ensure that they all look at the same fresh data. We might want them to interact with each other. Think about co-editing of documents on Google Drive, chat applications etc. We’ve implemented a simple solution for seamless synchronization of application state in real time, using a NoSQL database hosted in the cloud.

The complete source code of the example application can be found at https://bitbucket.org/letsdebugit/synchronize-vue-app-instances-with-pouchdb.

The Challenge

A group of students of Royal Academy of Arts in Hague, Netherlands, recently asked me to help with their ambitious study project. Amongst many features, they wanted to have real-time interactivity. Visitors would interact with the website and see actions of other users at the same time.

Project mentor has suggested vanilla JS and rolling out an API with a web-socket server. This seemed too complicated. Brilliant artists as our students are, they’re not IT professionals. I proposed a simpler solution:

  • Use a lightweight web framework, to simplify code and get good control of application state
  • Automatically synchronize the state between the running instances, using an off-the-shelf NoSQL database in the cloud

The Stack

We’ve quickly settled down on the following:

  • Vue JS for front-end, as it’s simple and it can be pulled from CDN, without any build process
  • PouchDB - in-browser CouchDB instance with automatic synchronization to a remote database
  • IBM Cloudant - a free CouchDB database in the cloud

Vue JS and PouchDB are JavaScript libraries which can be pulled directly from CDN, without build. IBM Cloudant gives us a free CouchDB instance with 1 GB worth of storage. PouchDB gives us local storage, trivially simple API, and automatic synchronization with a master database in the cloud.

With this intriguing technology stack we were able to roll out a working chat application within one hour, for the starters. This was just an example which will serve them as a boilerplate for all other types of interactions. Below we discuss how it’s been made.

The Architecture

Architecture for state synchronization between application instances is very simple:

Architecture

All data is loaded, edited and saved only to the local PouchDB database (which internally uses browser’s IndexDB storage). PouchDB database takes care of bi-directional synchronization with the master database in the cloud - there’s no need to write any code for it!

Prerequisites

We’ve subscribed to IBM Cloud and added Cloudant service instance under free tier. We went to the Cloudant Dashboard and created a new empty CouchDB database named cloud-chat. As recommended by IBM, we created a partitioned database, where document identifiers will be prefixed with type, for example message:9820981390812398. This is supposed to give much better performance and lower the costs, should the application ever go commercial:

Cloudant Database

In Cloudant Account Settings we’ve changed CORS settings to accept requests from all domains, as pictured below. Warning! This is OK for development, but for production you should only allow requests from your website domain!

Cloudant CORS Settings

Now you need to create access credentials for the database. Go to Cloudant service instance, Service Credentials and click New Credential. Give it a name, select Writer role and press ADD. Credentials record is now created. Expand it to see your user name, password and the URL of your Cloudant instance. Take note of them, you will need them in your code to create URL for connecting to the database. To run the code, you need to provide your own Cloudant credentials in database.js file.

Warning! In production application we wouldn’t use this URL directly from client code, because your credentials are at risk. We would rather proxy calls through a web server where the UI application is hosted, or use other ways of securing access to your online database.

Cloudant Credentials

Implementation

Connection to a local PouchDB instance is simple. We instantiate PouchDB object with a database name. In a similar fashion we connect to master database in the cloud - using URL obtained from IBM Cloudant dashboard. Then we instruct the local database to stay in sync with the remote database:

const dbName = 'cloud-chat'
const dbUrl = 'https://username:[email protected]/cloud-chat'
let local, remote

function connect ({ onConnected, onChanged, onError }) {
  local = new PouchDB(dbName)
  remote = new PouchDB(dbUrl)
  local.sync(remote, { live: true, retry: true })
    .on('change', () => onChanged())
    .on('error', error => onError(error))
  onConnected()
}

Loading the chat history requires one call to PouchDB and a bit of mapping and sorting. Just remember that messages are always retrieved from the local instance! There is no need to reach to the remote instance. Local database will be automatically synchronized with the remote database in a very efficient way.

Notice how we specify document key prefix message: when fetching data. This is to prevent fetching other types of records. Our database might contain more than just chat messages after all!

async function loadMessages () {
  const data = await local.allDocs({ include_docs: true, startkey: 'message:' })
  const messages = data.rows.map(row => row.doc)
  messages.sort((a, b) => a.timestamp > b.timestamp ? 1 : -1)
  return messages
}

Saving messages is done with put() to the local database. Local database will automatically send these changes to the remote database. Other connected applications will soon receive notification about these changes and will update themselves.

function saveMessage (sender, text) {
  const timestamp = new Date().getTime()
  const _id = 'message:' + sender + '-' + timestamp
  const message = { _id, timestamp, sender, text }
  local.put(message)
}

The VueJS chat application is not very complicated:

  • User enters his nickname and proceeds to chat window
  • Chat history is loaded from local database
  • Users enters messages and submits
  • Message is saved to local database
  • Local database synchronizes itself with master database
  • Other application receive changes from master database and update their UI

For brevity, we’ve removed all non-essential things from the code snippet below. Please refer to the git repo for the full source code:

import { connect, loadMessages, saveMessage } from './database.js'

const App = {
  data () {
    return {
      // Logged-in user
      nickname: '',
      // Chat history
      messages: [],
      // Entered message
      message: ''
    }
  },

  methods: {
    // Load messages from database into chat history
    async load () {
      this.messages = await loadMessages()
    },

    // Send the entered message
    send () {
      saveMessage(this.nickname, this.message)
      this.message = ''
    }
  },

  created () {
    connect({
      onConnected: () => this.load(),
      onChanged: () => this.load(),
      onError: error => console.error(error)
    })
  }
}

The JavaScript code is wired to a simple HTML markup:

<main>
  <section class="chatroom">
    <header>
      <label>Welcome, {{ nickname }}, and be nice!</label>
    </header>

    <section class="form">
      <label>Message:</label>
      <input type="text" v-model="message" @keypress.enter="send()">
      <button @click="send()">Send</button>
    </section>

    <section class="chat">
      <p class="message" v-for="message in messages">
        <i>{{ message.sender }} at {{ timeOf(message)}}:</i>
        <br>
        {{ message.text }}
      </p>
    </section>

    <footer>
      <button @click="logout()">Log out</button>
    </footer>

  </section>
</main>

End Result

Run index.html using a web server of your choice and voila, we have a multi-user chat application running on IBM Cloud!

References

The article is also available at my blog Let’s Debug It.

The complete source code can be found at https://bitbucket.org/letsdebugit/synchronize-vue-app-instances-with-pouchdb. Feel free to clone and reuse this code. Any suggestions or questions are most welcome!

LICENSE

Copyright 2020, Tomasz Waraksa

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.