Creating a Retrospective Board with


· ·

With more and more teams working remotely, tools have sprung up everywhere, shifting online processes. The fun part behind retrospective planning poker and other methods was always the interactivity, seeing cards move around and identifying barely readable scribbles.

As such, let’s take a look at how we can use data-sync to create a real-time retrospective board that supports both desktops and mobile phones. It will look something like this:

In the spirit of agile approaches, let’s start by breaking down our requirements. Our retrospective board needs to allow us to:

  • Add, edit and move cards
  • Allow everyone with access to the board to see live updates
  • Have a mobile friendly interface

And to add a little bit of privacy in case you decide to make the application public, let’s throw in a few security requirements:

  • Only let people access the board with a username and password
  • Only let cards be edited by their creator.

To achieve this, we’ll be using good old jQuery on the frontend and as our backend.

Setting up a server

Installing deepstream is as simple as downloading the latest version from the site or installing it on Linux via apt or yum.

Configuring credentials

Once we’ve got the server, we want to make sure that only users with a valid password can log in. Deepstream supports a number of different authentication strategies, but we’d like to keep things simple. For this tutorial, we’ll simply provide a file with usernames and hashed passwords. To use this, we need to switch to file authentication.

This is done in deepstream’s config.yml file, which can be found either in conf/config.yml or in /etc/deepstream/conf/ on Linux. Here, change auth type:none to the following:

# reading users and passwords from a file
 type: file
   path: ./users.yml # Path to the user file. Can be json, js or yml
   hash: 'md5' # the name of a HMAC digest algorithm
   iterations: 100 # the number of times the algorithm should be applied
   keyLength: 32 # the length of the resulting key

You’ll find a default users.yml file in the same folder where you can add your users, and any user-specific metadata. We’ll use it to define the users’ roles:

  password: "qQoEMakcnxfq6AQhQl2N9XiIke/JjTqDh+x7OMd5XQE=gEwXQZOWaHilsZ0fpllcOA=="
    role: developer
  password: "qQoEMakcnxfq6AQhQl2N9XiIke/JjTqDh+x7OMd5XQE=gEwXQZOWaHilsZ0fpllcOA=="
   role: scrum-master

To generate password hashes, just use deepstream’s command line interface via the executable:

# or .exe for windows
deepstream hash cleartext-password

And finally, start the server via:

deepstream start

Creating the board

Connecting to deepstream

Great! Now that we have a server running, let’s look at setting up the board.

The first thing we’ll need to do is let our users log in. To do this we’ll need to get the deepstream client. You have a few different flavours of installation, either using npm, bower, or having it come as part of your react or polymer plugins. For this example we’re going to go with the simplest approach of just fetching it directly from a CDN:


Now that we have the deepstream library included, we’ll need to get it to connect to the server.

const client = deepstream( 'localhost:6020' );

Since we want to limit users who can use the board, we are going to have to get the username and password from a minimalistic login form:

Which on submit triggers a login to the board:

$( 'form' ).on( 'submit', function( event ){
var authData = {
  username: $( 'form input[type="text"]' ).val(),
  password: $( 'form input[type="password"]' ).val()

ds.login( authData, function( success, loginData ) {
  if( success ) {
    // highly sophisticated desktop / mobile detection :-)
    var isDesktop = $( window ).width() > 800;
    new Board( ds, isDesktop );
    $( 'form' ).hide();
   } else {
     $( '.login-error' ).text( loginData ).show();

And that’s part two. You now have your users connected and logged into deepstream!

The juicy parts

Now comes the fun part—getting all the cards to remain in sync across all browsers/phones. Let’s take a step back and first look at data-sync and how it is used. We’ll be using records and lists to keep state. A record is just a convenient way of storing and manipulating JSON with data-sync built in.

Core concepts:

  • A record has a unique identifier. You can create your own or use getUid() to generate one for you:
const recordName = client.getUid();
const record = client.record.getRecord( recordName );
  • You can set its data:
record.set( {
 owner: 'john'
 position: {
   left: 375,
   top: 250,
 content: 'This card is awesome!',
 type: 'glad'
} )
  • To get data:
console.log( record.get( 'position.left' ) ) // 250
  • To subscribe to changes:
    record.subscribe( 'position', ( newPosition ) = {
      console.log( 'Card moved!' )
    } )

Deepstream also has a concept of a “list”, which is a useful way to maintain a set of records that have things in common. You can addEntry( removeName ), removeEntry( recordName ) and listen to entry-added and entry-removed events.

Let’s take a look at how we can put these things together to make a board. We’ll need a list to contain the set of all the records on the board. Whenever a card is created, the list will notify us and we can add it into the DOM. Since the list is the entry point to all our records, we need to use a non-random name.

// Creating a card
function createCard() {
 const cardId = 'postits/' + this.ds.getUid();
 const card = this.ds.record.getRecord( cardId );
 card.whenReady( ( record ) => {
 record.set( properties );
 this.cardList.addEntry( cardId );
 } );

// Adding a card to the dom
function onCardAdded() {
 new Card( /*...*/ );

// Creating all the existing cards on login
this.cardList = this.ds.record.getList( 'example-board' );
this.cardList.whenReady( ( this ) => {
 const entries = this.cardList.getEntries();
 entries.forEach( onCardAdded.bind( this ) );
} );

// Listening to card being added on the board.
this.cardList.on( 'entry-added', onCardAdded );

That covers most of the creating of the board. The demo code on GitHub fills in all the parts I’ve not covered here, such as removing cards, and different ways you can add cards depending on your input devices.

The final requirement is dragging a card around and seeing it update on all other browsers. You can see this being done here:

 .css( 'position', 'absolute' )
 .draggable( {
   handle: ".card-header",
   zIndex: 999,
   // Prevent jQuery draggable from updating the DOM's
   // position and leave it to the record instead.
   helper: function(){ return $( '' ); },
   drag: ( event, ui ) => {
      this.record.set( 'position', {
       left: ui.position.left
    } );
 } );

this.record.subscribe( 'position', ( position ) => {
  if( position ) {
    this.element.css( {
       left: position.left,
    } );
}, true );

Note how we prevent jQuery from updating the DOM directly. This is because we are using the record as our single source of truth. By doing so, our code will process things the same way, regardless of whether the action happened remotely or locally. This is a great way to consume changes—otherwise, your code will become cluttered with unwanted conditions.

Perfect, now our board starts to look presentable! Let’s look at adding a tiny bit of permissions now that we are familiar with the card’s JSON structure.


Deepstream comes with a powerful permissioning language called Valve, which can be used to create rules to allow/deny all possible client actions. Going back to our last requirement, we want to only allow the creator to update his/her own cards. Since we’ve made the username part of the recordname, this is rather straightforward. Let’s take a look at how we can implement that in our pemission.yml config file.

  write: "data.owner ==="

And that’s it. The users who can edit cards have to be the same as the cards’ creators.

Let’s introduce a tiny bit of scope creep, and allow the scrum-master to edit any card and be the only one who can delete cards. Luckily, we added this earlier on in the user config, so we have access to user roles.

    write: "data.owner === || === 'scrum-master'"
    delete: " === 'scrum-master'"

And done! We now have all the bits and bobs of a real-time, authenticated and permissioned retrospective board!


Click on a tab to select how you'd like to leave your comment

Leave a Comment

Your email address will not be published. Required fields are marked *

%d bloggers like this: