Spartez Software has joined the Appfire family. Learn more

A tiny enterprise app: thank you, Google Firebase!

Roman Lutsiv
Roman Lutsiv
Oct 4th, 2021

This article describes how Google Firebase made Agile Poker possible. The team developing the product shares knowledge about real-time events, data structure, security, and scalability along with sample code snippets.

Agile Poker (AP) is a highly scalable, enterprise-ready app used by 600+ companies with infrastructure costs below 200 USD monthly. Thanks to Google Firebase we can serve x1000 more customers with a minimal cost increase.

One image is worth a thousand words. Or Architecture Diagram


Agile Poker (AP) is a Node.js app with a frontend written in Vue.js. The frontend part is a SPA (single page application) that communicates with Firebase directly. A great part of application logic resides here — we made the backend redundant once the application is loaded and the Firebase access token is generated.

We run 2 web nodes for the backend from the Heroku cloud platform, though we could easily survive with a single node. We added the second one purely for reliability reasons: we have observed short glitches in-app availability in the case of web node maintenance performed by Heroku. Backend traffic is minimal: around ~3 req/sec during peak load times. But most of the time, it remains under 1 req/sec.

We cache frontend resources (JS / CSS) for the specific release version. Every release generates a new version that instructs AP to download the updated frontend packages. Application settings are cached in Redis (we could use Jira for that, but you can guess Redis is much, much faster — milliseconds vs. tens or hundreds of milliseconds).

The scheduler is needed for sending emails (that part of AP architecture was omitted from the diagram above as we wanted to focus on the most important things). Postgres DB could be easily replaced with Firebase, but it’s there for historical reasons, and we just keep it as is. We store information about Jira tenants there, as well as tenanted JWT secrets used to sign requests to the specific Jira instance.

Tell me how it works

The core part of the app resides in the frontend and Firebase. Agile Poker is a Jira app, so for the authentication part, we rely heavily on Jira:

  1. With the initial page request (index.html), we receive a signed JWT token from Jira; it contains the Id of the Jira instance and user Id. The token is validated on our backend, using the JWT secret we stored when the app was installed on Jira.
  2. We generate a custom Firebase authentication token on the backend and sign it using the Firebase private key to secure it. The token can contain custom metadata; in our case, it is Jira instance Id, User Id, and board Id.
  3. The custom token is added to the page inside the <meta> element. Firebase SDK on the frontend exchanges it with Firebase authentication API to obtain an access token and refresh it regularly.
  4. The access token is then used for all requests to Firebase. All communication happens directly from the frontend; the backend is not needed for that.
  5. Access to data stored in Firebase is controlled through built-in Security Rules. Rules define read and write access through boolean expressions, based on metadata included in the token, data location (e.g., node name matching Jira instance Id), or property values (e.g., the value of administrator user Id).

Real-time updates

Communication with Firebase is asynchronous and bi-directional. That’s how the users are informed of the actions performed by other users. Our frontend listens for updates in Firebase DB, and we can show the changes immediately after state changes.

Firebase data structure

We store data per tenant (customer), per Jira board, in the objects we call sessions. We differentiate public and private sessions, as well as active and completed ones. How did we design a data structure to retrieve necessary data as effectively as possible? Keep on reading…

The first rule of thumb is to model your data structure to reflect what you want to show in the app. Since we wanted to show a list of all public and private sessions that the user participates in, we have started simply:


The question is how to query all public and private sessions for the specific user. With the above data structure, you could easily query for all public sessions like this:


But this won’t work for private sessions as you first need to check if the user is in the participants' list, and Firebase queries can only order by one key at a time. For that reason, we keep two relevant lists in Firebase:

  1. Users list with private session ids for every user,
  2. Sessions list with participants ids for every session.

Having two lists may look like data duplication, but it is totally fine in NoSQL solutions. It’s called data denormalization, and it can be used to load the two-way relationship data efficiently. You can find more about this here in the official Firebase documentation.

So now we have another dimension of the user-to-session relationship:


Now we can simply load the list of private sessions for the user with:


And then loop over the session keys to load each private session details.

Backend logic through Cloud Functions

Firebase is basically a backend as a service. For example, the previously mentioned Firebase Database is completely serviced, managed, and scaled automatically by Google Firebase. But what should you do in case of some operations that don’t fit the frontend, for example, updating user roles, sending emails, etc.?

Firebase has Cloud Functions to do exactly that. You can write code that runs on Firebase servers and interacts with other Firebase services.

In our case, we implemented a code that watches for the session’s archival events and triggers a function that moves that session to a separate node.


Security rules

We have implemented security rules based on Jira instance Id, board Id, User Id, session access (public or private), session configuration (e.g., moderator user Id, list of session participants).

For example, to validate if the user can change the name of a private session, we’ll check:

  • if Jira Id from access token matches Jira Id in session’s reference path,
  • if board Id from access token matches board Id in session’s reference path,
  • if user Id from access token matches moderator Id stored in the session configuration.

The first two checks are common for most read and write operations; the last one is specific to the particular operation (business logic) — only the session moderator is allowed to change the name.

Show me more code

If you keep reading till this point — it’s time to show you more fragments of our code to understand better how easy it is to integrate with Firebase.

The code samples below are coming straight from the Agile Poker codebase. However, they were purged from the unrelated fragments to focus on the Firebase integration itself.

Authentication

The first thing we will need to get data from the DB is to generate the Firebase token. We do it on the backend, using the firebase-admin library. We include it in the response of the "/index" endpoint


It is stored in the meta tag in the page’s head section together with other Firebase parameters.


Then on the client's side, we can extract the values from the meta parameters and use them to initialize the connection and authenticate to Firebase DB.


We then use the auth() function when requesting data from Firebase (for example, to load the Agile Poker session). After that, we get the data reference by specifying the path to the location where it is stored.


The reference can be used for reading or writing data to a specific database location.

Subscribing to data changes

A great feature of the Firebase Realtime Database is listening to the database changes and updating the application state immediately. This allows us to provide live interaction between users.

To do so, we define a listener watching for the data changes. We pass the data location params (boardId, sessionId, path) and the callback into the function.

Later, we use the function above and pass the Vuex mutation as the callback.

In the next step, we use it inside the UI component. In the mounted lifecycle hook, we finally call the subscription and start listening to changes. From now on every change in the jira/${jiraKey}/board/${boardId}/sessions/${sessionId}/voting/issueId is going to put the updated value in the application’s state.

The last thing we need to remember about is unsubscribing from the changes when the user leaves the page.

Why Firebase? Or … you should start from it!

Let’s clarify — this article is not sponsored by Google / Firebase. :) We found their technology so great that we expanded our usage of Firebase and Firestore in other products. Why?

  • Ease of implementation. Documentation is great, the code is simple, and it just works as expected.
  • Designed for multi-user activities. It matched our use case precisely — we are implementing software for online collaboration (estimation), and Firebase made it so much easier to handle the multi-user activities in real-time.
  • Security. We define security rules based on data, and Firebase does the rest.
  • Scalability. Firebase scales seamlessly and without any action needed from our side — going from 1 req/sec to 20 req/sec corresponds to load average increase from ~0 to 0.1. Firebase also supports sharding for large setups, but we are not getting even close to the limits. In our case, concurrent connections spike to 300 while a Firebase limit before you should consider sharding is 200K connections.
  • Price. With our current usage, Firebase cost is ~2 USD per month (yeah, the remaining ~198 USD is spent elsewhere, Heroku mostly: web nodes, logs as a service, and Postgres DB). You should pay attention to how you design the Firebase schema and what data you store in there. In our case, those are session configuration, references to Jira objects, and estimation info. We used less than 200MB for a few years of running in production.

Sounds interesting? We are hiring!

Hungry for success and new challenges? Join the ranks of Agile Poker's team!

Join us as a Technical Team Leader and help us deliver exciting solutions for our customers and explore opportunities outside the Jira/Atlassian marketplace!

_________________________________________________________________________________________

We have partnered with JustJoinIT to publish this article in Polish here.