Skip to main content

Architecture | Multi-User Applications with PouchDB and IBM Cloudant

Introduction

Imagine a web application used by many users at the same time. Sometimes we want to be sure they’re kept in sync. We want to ensure that they look at the same fresh data. We want them to interact with each other. Think about co-editing of documents on Google Drive, chat applications, shared whiteboards 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 asked me for help with their ambitious art project. A website which, among many features, allowed for real-time collaboration. Visitors would interact with the website and see actions of other users at the same time.

The project mentor suggested vanilla JS, an API server and web sockets for client-to-client communications. This was too complicated. Brilliant artists as our students are, they’re not IT professionals. I proposed a simpler solution:

  • Use a light web framework, to simplify the code and get good control of application state
  • Automatically synchronize the state between visitors, using NoSQL database with replication capabilities, hosted 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 used directly from CDN without any build process
  • PouchDB - in-browser offline CouchDB client with automatic synchronization to a remote database
  • IBM Cloudant - which offers a free CouchDB database in the cloud

Vue JS and PouchDB are JavaScript libraries which can be used directly from CDN, without complicated build process. Vue JS greatly simplifies application code and state management. IBM Cloudant gives us a free CouchDB instance with 1 GB worth of storage. PouchDB provides 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. This example would then be used by our students as a boilerplate for all other types of interactions. In the chapters below we present the initial example.

The Architecture

The architecture for state synchronization between clients is very simple:

Architecture

All data is edited and saved only to the local PouchDB database, which internally uses browser’s IndexDB storage. PouchDB client 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 subscribed to IBM Cloud and added Cloudant service instance, available 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:password@username.cloudantnosqldb.appdomain.cloud/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!