Adding server-side functions to my tiny responsive web apps

A month or so ago, I wrote a story explaining the stack and tool-chain I’ve put together for building tiny responsive web apps.

The technology I chose solved these problems:

  • I only want to pay for what I use (scale-to-zero)
  • I don’t have a lot of time available for learning or building (no steep learning curves without substantial time-savings)
  • I don’t have time for maintenance activities (no patching servers, automated scale-up)
  • I’m not a good UI designer or front end engineer (design systems are great)

My stack works really well for an app with standard create/retrieve/update/delete operations, but I recently hit a requirement needing more sophistication on the back end.

The business problem

In my original story I used the example of an app I’ve built to help with mindfulness meditation. I built the app to provide these features:

  • A timed, unguided meditation
  • Ability to continue when the timer has finished if wanted
  • Ability to pause and resume a session
  • A calming, ambient, background sound loop
  • A periodic bell to check I’m still in the moment
  • Random intermittent, but relevant, sounds
  • An automatic diary of sessions

The new requirement

My app is working really well, but I decided it was about time to add some stats to provide myself with further encouragement. A common motivation statistic for wellbeing and fitness apps is to show the longest “streak” that a user has managed. In the case of a meditation app, the stat is the longest number of consecutive days that a user has meditated.

My current tool-kit looks like this:

It’s a two-tier architecture, so all the business logic lives client-side in the browser. This is fine for the current requirements, but things change when I need to compute statistics. I could stick with this stack and count the streaks from the sessions in my diary client-side, but I’m getting close to 200 sessions in my diary now. If I play that forward a couple of years, then to render my stats page, I’ll be retrieving a lot of data from Firebase Realtime Database every time it loads and unnecessarily repeating the calculation in the browser. If I one day have a whole lot of users, then there’s going to be a lot of data unnecessarily being retrieved a lot of the time, and that costs money.

Firebase triggers

Firebase is remarkably rich in functionality. I explained how I used Firebase Functions (Google Cloud Platform Functions) in another story to build a REST API. Firebase Functions includes the ability to register functions as database triggers. Rather than triggering the function in response to an HTTP call, a function can be triggered when data is created, updated, or deleted in a database. The code runs with admin privileges, and has access to the user principal that triggered it, plus a snapshot of the data, so it’s perfect for keeping tables of stats up to date in response to data changes.

I followed the example using the onUpdate() trigger detailed in the docs. I wrote a function to figure out the longest streak from a list of diary entries and hooking it up was just a matter of grabbing the entries from the after field on the snapshot provided on update and writing the result back to the parent node. Every time a user adds or removes a meditation session, the stats for that user is automatically recalculated. With the stats just sitting in the database, my app can read the result via the usual Firebase API calls.

exports.countStreaks = functions.database.ref('diary/{userUid}/entries')
.onUpdate(async (snapshot, context) => {
const afterEntries = Object.entries(snapshot.after.val());
const processedEntries = [];

for (const [key, value] of afterEntries) {
const finishTime = DateTime.fromMillis(value.finishTime);
const diaryEntry = {finishTime, totalSeconds: value.totalSeconds};
processedEntries.push(diaryEntry);
}
const stats = statscalculator.longestStreak(userTimezone, processedEntries);
if(stats){
return snapshot.after.ref.parent.child('stats').set(stats);
}
});

Summary

So that’s it. I can deploy the function with a Github Actions CI/CD pipeline as I described in my story on building an API using Firebase functions. I get scale-to-zero thanks to the GCP usage-only charging, and my code is still in Node.js, so no new languages to learn!

Boom.

Technology leader for Xero in Auckland, New Zealand, former start-up founder, father of two, maker of t-shirts and small software products