Case Study

How StretchMinder uses Firebase

Calvin Cheng
Firebase Developers
6 min readNov 9, 2021

--

When I started working professionally as a developer, sitting like a rock for hours at my desk, deep into the code was a norm. I was taking too little time for breaks, if any, to just grab a drink of water and then propped right back into my seat. Little did I know the damage that was being unleashed to my body and the resulting pain that this lifestyle was causing.

I was having aching neck and shoulders, tight hips, shooting pain down my arm, and constant headaches. Getting through the day started to get extremely draining. That’s when the idea of StretchMinder was born. The mobile app designed to help combat long uninterrupted periods behind a desk. It makes it extremely easy and cognition-saving, by guiding you through a short series of exercises that can be done right at your workstation.

Today, thousands are using StretchMinder to increase their daily dose of physical activity, reduce pain, relieve stress, and improve energy and focus through integrating bite-sized ‘Activity Breaks’ into their day.

In this blog post, I want to give a sneak peek behind the scenes of how we built StretchMinder as a small team of 2. So, grab a glass of water, kick back, and enjoy the story.

Choosing a PaaS Solution

As the only developer in our team, it was critical to keep the tech stack lean. Our vision to create a great product with limited resources required focus, which meant putting the majority of our attention towards making the experience seamless for the end-user.

To me, that meant leveraging third-party services that would essentially carry the load of such responsibilities — like dev-ops and development of backend services and make it as easy as possible to focus on the main product which was the app itself.

When considering between other PaaS solutions, Firebase stood out as a clear winner due to its SDKs’ abilities to provide offline access for free, streaming of document changes on the client via Cloud Firestore, and the out of the box functionality like Cloud Functions, Remote Config, In-app Messaging and A/B testing.

Our mobile app is built using React Native with Typescript, and we integrate with all Firebase services using the officially recommended collection of packages from react-native-firebase.

How we use Firestore

As an app that relies more heavily on in-app content and client-side functionality, the requirements on the backend were fairly simple. As we plan on supporting multiple devices and platforms, having user accounts is the only thing needed to store account history and preferences and synchronize it across devices.

We’ll go over how we came up with a part of our data model which enables StretchMinder to keep track of a user’s history of Activity Break completion and their progress towards their daily goals.

Keeping track of Activity Breaks

With StretchMinder you can set your daily goal for the number of Activity Breaks you want to complete each day. We wanted to be able to display how well you’ve been committed to breaking your sedentary habits through a Goal Progress view so that you can be motivated to keep active throughout the day.

Profile — Goal Progress view

In order to be able to store historical data like this, there were many possible ways to go about this. We wanted to be able to keep a history of all the activity breaks a user has ever completed within the app, along with their completion progress towards their goals.

To do this we made a history sub-collection within each user, which is indexed by the current month with the format YYYY-MM:

users/{userID}/history/{YYYY-MM}
-- activityBreaks {
[yearMonthDay: string]: {
completed?: ActivityBreakRecord[],
goal?: number
}
}

This enables us to store an unlimited set of historical stats for a user, which is easily queryable by the given month.

We made an assumption that a user would never complete so many Activity Breaks within a month that it would fill up an entire 1MB document size (let’s hope this doesn’t turn out to be too costly of a mistake).

For each Activity Break that is completed, logActivityBreakHistory() is called and a new object keyed by the current day in the format of YYYY-MM-DD is created within the map for the given day if it does not already exist. Otherwise, a record of the completed Activity Break is appended to the completed array, alongside the currently set Activity Break goal.

Since a user may adjust their goal higher over time as they progress, keeping the goal attached to a specific day instead of a property in the user’s preferences allows users to modify the goal without affecting their historical completion progress. This allows us to have a complete historical record of the number of Activity Breaks a user has completed on any given day, plus the recorded goal at that point in time.

Otherwise, when modifying the goal to a larger value in the future, and not keeping a history of the goal, we would not be able to see how close to their goal they were in the past.

Now to fetch the user’s history for the chart, all we need to do is fetch the history document from the user, referenced by the month:

Although we could have made this simpler and recorded all the completed Activity Breaks and user’s goal history in their own collections, doing so would mean needing to fetch multiple documents to build out this view, or to write a Cloud Function to serve the data in a single response, which also requires multiple document fetches.

This model was an obvious winner for us as it made the most sense for the needs of the UI at its current state.

Aggregating lifetime stats

Alongside recording the historical progress towards a users goal, we wanted to display their overall lifetime stats.

Profile — Lifetime stats

To make that possible, we needed to keep an aggregated count which would total the amount accumulated over time.

Here’s a map that we added inside of the user document:

users/{userID}
-- stats {
activityBreak: {
totalTime: int
sessions: number
}
}

Upon logging a completed Activity Break, instead of calling logActivityBreakHistory() directly, we instead call logActivityBreakCompletion():

In this operation, we need to write to multiple documents at the same time as an atomic operation — that is, we need both the writes to occur, and if one fails, the entire operation should fail. This requires us to create a write batch when performing multiple writes as a single atomic operation.

By passing the write batch to both logActivityBreakHistory() and setUser(), they both set their operations within that batch instance, and are executed atomically when batch.commit() is called.

Incrementing a value

FieldValue.increment() returns a special value that can be used with set() or update() that tells the server to atomically increment the field’s current value by the given value. There’s no need to fetch the current value and update based on the previous value.

Conclusion

With Firebase, a small team like us is able to have a flexible and scalable backend infrastructure and the tools necessary to be able to focus on building out the part that matters most which is the end user experience and making sure that the app solves a real need for our customers. I believe that it’s tools like this that enable us smaller indie developers to create businesses which in the past required a much larger concerted effort.

I don’t see Firebase as a tool just to get your first MVP out there, but as a full-scale production ready service that you can leverage to build a world-class product which is exactly what we hope to be building with StretchMinder.

--

--

Calvin Cheng
Firebase Developers

Founder of StretchMinder. Lifelong learner, builder, health & productivity nut, martial artist and enthusiast of the esoteric.