Building a Planning Poker App

A few months ago, I built a planning poker app that teams in my organization have been using to estimate JIRA tickets.


I built the app for a few reasons.

We needed to estimate tickets

First and foremost, our team didn't have a reliable tool for this. We were using Hatjitsu for a while but started seeing some performance issues and bugs where we'd need to create a new room every time we estimated. We tried out other apps but the good ones eventually put up a pay wall.

Learning opportunity

I wanted to learn more about and web sockets but I was waiting to build something until I could build something practical. This project seemed like a great fit.

Time was on my side

It's usually not enough to want to learn new tech and have an idea for something to build. You also need the time to learn and build it. Luckily for me, I had the inspiration to build this app on a weekend with nothing going on.

The app

The application itself is fairly simple.

As a user you can either be a voter or a spectator. Spectators can see the board, reveal the current estimates and reset the board. Voters can perform the same actions as a spectator but can also submit estimates.

Check out the production app.

Check out the source code.

The tech behind the app

This application was built using the Remix framework which leverages Node.js on the backend and React on the front end.

Server side

On the server there is a single web socket server instance that clients can connect with.

const wss = new Server({ server });
wss.on('connection', ws => {
ws.on('message', message => {
console.log('Received message:\n\t %s', message);
wss.clients.forEach(function each(client) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(message, { binary: false });

Every message from a connected client is broadcast to every connected client.

This web socket server is only persisted in memory. If the server goes down, all of the web socket connects go down.

Client side

The client side is where most of the application logic lives. There are two routes, a home page and a page for a "room".

From the home page users can create a "room" and navigate to it. A room can be thought of as a distinct group of users connected via their own pool of web socket connections. Users share the unique URL for the room they want their teammates to join.

The code for each room establishes a web socket connection for a user, coordinates incoming as well as outgoing socket messaging and manages the majority of the UI state.

Event handlers

When the page for a room is first mounted the web socket connection is instantiated. There is logic to ping a message every 30 seconds to keep the web socket connection alive (more on this later). Critical event handlers are set including:

  • connecting to the socket
  • receiving messages
  • leaving the room (beforeunload and onhashchange events on the window object)

Client data models

Clients pass messages to each other with information about players, estimates and other board status updates.

interface Message {
userId: string;
roomId: string;
isHidden: boolean;
estimate: number | null;
isSpectator: boolean;
reset?: boolean;
playerLeft?: boolean;

These messages are used to manipulate the state of the application and sync state with other clients, including Player state.

interface Player {
id: string;
roomId: string;
isSpectator: boolean;
estimate: number | null;

Processing messages

The even handler for processing messages is where most of the client-side orchestration happens. One message is processed at a time.

Validation is immediately performed on a new message. Messages from other rooms are ignored. In hindsight, this validation should probably happen on the server side.

The following state updates are processed depending on the data in the message being received:

  • updating player estimates
  • hiding/showing estimates
  • adding players
  • removing players
  • resetting the board
  • updating spectator status

What could be improved

I built this app over a weekend (for the most part). As with any other software solution, there is always room for improvement.

Adding automated tests, in general, would be an improvement. I'm guilty of rarely writing tests for my side projects. It's a bad habit that negatively affects me every time I revisit a project after some time.

I'm not very confident making changes to a codebase without tests that I haven't touched in a year. The time I spend manually testing would vanish if I just spent a bit of up-front time writing automated tests.

Here are some implementation details I think could improve the planning poker app:

  • Extracting logic into smaller files for the room logic would be beneficial for readability and testability
  • Using a state machine to manage all room state permutations would be nice
  • Should probably validate that messages are only sent to their respective room on the server side

The lessons learned

I've learned a few things from building and using this app. Many of my learnings were actually just reinforcements of concepts I was already aware of. But practical use cases like this app help solidify my understanding of some of these concepts.

Hosting matters, especially on a budget

Using the app when developing locally isn't the same as using the hosted version. Seems simple, maybe obvious.

Your application might behave differently depending on the hosting environment. I originally deployed this app using Heroku. Apps that haven't been visited in a certain amount of time have to "warm up" when using the free pricing tier of Heroku. This resulted in users waiting around 6 seconds before anything loaded when visiting the app for the first time.

I've since switched the hosting provider. Now the app is hosted on The free tier allows for the app to be accessible instantaneously.

Works on my machine

Testing the app with a single user who is emulating multiple users isn't the same as having multiple players in a practical setting. Seems simple, maybe obvious.

For example, did you know that web socket connections close after being idle for 60 seconds? I didn't.

The first time my team used this app everyone had to refresh the browser tab before we started voting on a new ticket to re-establish the socket connections. Since we chatted about tickets for more than 60 seconds before voting the socket connections all timed out.

I didn't catch this when testing by myself because I was just estimating, resetting the board and estimating again right away.

Remix gains were slight

I learned about the basics of building and deploying a Remix app but I definitely didn't need Remix for this app.

The app uses one data loader to load the room ID parameter but that's it.

It was nice to deploy one app instead of a separate back end and front end but managing separate deployments isn't a huge deal. Also, I could've deployed a single app with other frameworks that I'm familiar with (Phoenix, for example).

Even though I feel like I've barely scratched the surface with Remix it was still a great learning opportunity.