Table of Contents
- And Now…For a ‘Real’ App!
- Case Challenges
- Exhibit A: Current User Stories & Testability
- Exhibit B: New User Stories on Part Reviews
- Exhibit C: Unpacking & Implementing the Data Model on Firestore
- Exhibit D: Securing the Data Model
- Exhibit E: New & Exciting Stuff in the Client JS
- Exhibit F: Effectuating between Model Alternatives
- Acknowledgements
This is a case study for use in a degree program course: Coding with GPT.
Please note: This case builds on a prior case called ‘Creating & Managing Users with Google Firebase (Case)’ and assumes familiarity with its contents.
And Now…For a ‘Real’ App!
Frangelico DeWitt is working with his team to develop a new suite of software for their employer HVAC in a Hurry (a heating and air conditioning service business). Their current focus is on a web application that helps field technicians find availability and pricing for replacement parts.
Frangelico is not a developer by training, but, with support from his team, he’s been actively contributing to the app as a way to learn about going from design to code. To date, they’ve built out a View (user interface) with HTML & CSS, the early Controllers (algorithms) to implement their target UX, and they’ve even interfaced to a BaaS (backend-as-a-service) to handle authentication and user management. That BaaS, Google Firebase, also has a module called ‘Firestore’ which Frangelico and his team have decided to use to build out their data Model (database).
It’s not by chance that implementing the Data Model they need to support their target UX is the last big item before they have a ‘real’ app, a real working ModelView-Controller framework. The reason their addressing the Model towards the end of the their development is that:
a) it’s hard to know what data you need and how to model it until you’ve substantially worked out the UX
b) Model implementations (databases of various) types are relatively messy to change.
In order to make sure they not only get code that not only works, but also delivers for users, the team has generally followed a four-step process. In step one, they focus their design intent with user stories and prototypes. You’ll find the existing user stories they’ve been working in Exhibit A and the new stories (around item reviews) in Exhibit B.
In the second, they collaboratively unpack what they want to have happen in codeable steps (rough notes vs. code). In Exhibit C, you’ll find the teams notes and sketches on the target Data Model.
In the third, they evaluate alternative approaches and technologies, in their case with a heavy emphasis on buying or ‘renting’ what they need vs. building it from scratch. In Exhibit F, you can find notes on how they arrived at the decision to use a non-relational (noSQL) database on the Firebase.
In the fourth step, they iteratively code, test, and debug their work. In Exhibit C, you’ll find notes on the implementation in Firebase (along with their design notes). In Exhibit D, you’ll find some notes on key JS concepts they used to implement the Model-facing Controllers. In Exhibit E, you’ll find notes on how they set up and verified user access to the relevant data.
Case Challenges
<···CASE CODE···>
1. Can you follow the progress from focusing design intent to implementing the data Model?
The exhibits below step through the team’s design intent, unpacking the design, effectuating between alternatives and then implementing both the new data model as well as the updated views.
– Can you follow the process? Describe it to a colleague?
– Could you see applying this process to something else you’re working on now?
2. Can you follow what the JS on the new parts search page doing?
– There are a bunch of Firebase functions being called that aren’t listed in the Javascript. Where are those coming from?
– Where is the parts data that you see on the screen coming from?
– What is the role of DrawDiv? What are its inputs?
– How does the transition work when the users clicks from the parts summary page (Parts Search) over to the details for an individual part on the Parts Detail) page?
3. Can you follow what the JS on the part detail and review page is doing?
– This View also has dynamic components. What are they and where are they coming from? How does localStorage contribute to all this?
– This page has not only read operations but also a write operation (when the user submits a review). Where are those and how do they work?
4. Advanced & Optional: Refactor the ‘postReviews’ function
You’ll notice versions of the postReviews function on both of the case Fiddles (Parts Search & Parts Detail) that are very similar but not exactly the same. Let’s say you’re about to consolidate all this code and you want to make sure both Fiddles can use the exact same version of postReviews. How would you do that?
Exhibit A: Current User Stories & Testability
These explain the core intent for the team’s new web application.
Epic User Story
‘As Ted the HVAC technician, I want to identify a part that needs replacing so I can decide my next steps.’
Storyboard
Based on their observations in the field, Frangelico’s team thought through the user experience of the epic with the following storyboard:
Child Stories
User Story |
Key Questions and Results |
‘I know the part number and I want to find it on the system so I can figure out next steps on the repair.’ | How well does this search type work relative to the alternatives? How often is this search used per transaction relative to the alternatives?Metrics: Searches of this type relative to others Sequence of this search relative to other search types Conversion to order from this type of search(%)Candidates for A/B testing: Add/remove UI for user story #3 and see how it affects click-through to subsequent screens. |
‘I don’t know the part number and I want to try to identify it online so I can move the job forward.’ | (see above) |
‘I don’t know the part number and I can’t determine it and I want help so I can move the job forward.’ | (see above) |
‘I want to see the cost of the part and time to receive it so I decide on next steps and get agreement from the customer.’ | How often does this lead to a part order? How will techs that do this perform relative to others?Metrics: Conversion rate to order Customer satisfaction per job of techs in a cohort that use the tool vs. baseline (mean customer satisfaction per job) Billable hours for techs in this cohort vs. baseline (billable hours per week)Candidates for A/B testing: Variations in UI and how they affect conversion to order |
‘I want to order the part so that we can move forward with the repair.’ | How often are tech’s ordering through the app? How does this affect cost drivers and customer satisfaction for the job?Metrics: Parts Ordered on App/Parts Ordered by TechnicianCandidates for A/B testing: Tech’s the have access to the tool vs. those that don’t: relative change over test period. |
Exhibit B: New User Stories on Part Reviews
The user stories here are:
‘As Trent the Technician, I want to see how my colleagues have liked a certain part so I can decide it’s a good choice or not.’
‘As Trent the Technician, I want to finish a review for a part I used, so I know it’s available to my fellow technicians.’
This image is the team’s low fidelity prototype of the first story about seeing reviews as users search for parts:
And this image shows a similar prototype of the page where I user can see part details and review a part:
Exhibit C: Unpacking & Implementing the Data Model on Firestore
The most important part of creating any data structure is knowing what you want to have happen. In practice, that’s hard to do well if you haven’t already worked through the UX, focusing and unpacking your point of view on the UI (View) and supporting algorithms (Controllers). The HinH team has done this work- you can see the user stories in Exhibit A and a working version of the site here and in the image below.
In fact, the HinH teams used various ‘stubs’ to serve as placeholder Models during their development process- first, that was just static HTML elements (with HVAC part data) and then it was a temporary ‘stub’ function that dynamically created the HVAC parts from a spreadsheet.
Now, the team feels they know enough about what they’re after with the Model to unpack it and implement it.
Unpacking a Data Model
Unpacking your intent for a data Model means describing to the best of your ability the natural, real life model of how the elements relate to each other in terms that are actionable for Model development. The diagram here is a take on this for the HVAC parts.
Essentially, parts and users have various properties, and the review of a part is always linked to a particular user. Given this, the next question is how to best map this to what’s available in Firestore.
Implementing the Data Model in Firestore
‘Firestore’ is a service available within Google’s Firebase suite of backend-as-a-service (BaaS) offerings. Firestore is a NoSQL (non-relational) database that’s abstracted for developers to interaction with just a web-based admin console and a set of API calls. Better still, Firebase offers software development kids (SDK’s) for most major programming languages- basically, these are pre-written functions you can use in your code to further simplify your Model implementation and maintenance. For more on Firestore and why it’s hypothetically a good choice for HinH, see Exhibit F.
Turning our attention to how we render the now unpacked data Model on Firestore, you can see that their underlying heuristic is that all data lives as ‘documents’ in a ‘collection’. Each document can then have collections nested within it. In our case, each HVAC part is a document where they used the part number as the primary identifier (key) and where all those HVAC part documents live in a collection called ‘parts’. These documents then have fields, which you can edit here in the admin console or with a client app.
Note: the users collection you see is just for administrators. The rest of the user data is stored in the Firebase Authentication module and referenced where we need it in Firestore, as you’ll see below.
The reviews are then a collection nested within each HVAC part document. In the screen shot below, the Firebase Console has reoriented the view to show just the reviews collection, but you can see from the top left that this is still in the context (child elements) of the particular part.
You’ll notice that the relation from the reviews back to the user who created them is done by creating a ‘user’ field in each part and referencing the userID in the Authentication module.
Now, if you have a background in relational databases and normalization, you’re probably having a stress response and you probably think the reviews should be in their own table-like object that the parts then reference. This is just not how Firestore (and a lot of NoSQL DB’s) approach things- their view is that the flexibility and read performance of having the data in this form works well for the developer they have in mind. If you don’t have a background in relational databases, don’t worry about this, but take note that there are various different approaches to models. You can read about those in Exhibit F.
All that said, sometimes you may want to cross reference data that doesn’t live in your current document tree. The relationship between user data at large and the Firebase Authentication module is an interesting case in point that also deals with how and why Firebase’s different services interact. Previously, the team at HinH integrated the Firebase Authentication module to handle user authentication (account creation, resetting passwords, login, etc.). Now that they are deploying Firestore, there’s the question of where and how they organize data about users.
For example, when a user wants to pull up the full reviews on a given part where (we’ll assume) they’d like to see who wrote the various reviews, how do our Controllers get the user’s name to display it? Right now, the reviews document just has the unique ID of the user (UID). The Authentication module is (by Google’s design) emphatically not a full blown account and identity management system- it really just deals with authentication. However, you can add a ‘displayname’ and cross reference that in the Authentication module with the UID. For HinH’s purposes, that’s sufficient and their approach is to solve the problems/do the jobs they have in front of them and hypothetically refactor once they learn more about user behavior. However, if they wanted to have more data on users, they could create another top level collection in Firestore and cross reference data there with what Firestore calls a ‘DocumentReference’, which is just a reference to another collection and document.
This covers implementing everything the team unpacked about their data Model, except for the list of orders (per user), which the team has decided to defer until later. If any of this explanation is unclear or incomplete, I highly recommend using AI (chatGPT, etc.) to ask questions. Here’s an example of a few things I wanted to verify myself:
Alex’s Q&A about Firebase with chatGPT (o3-mini).
If you want to hear more about how Google themselves talk about Firestore and the reasoning for its design, this reference and video from them is also very good:
Reference on Firestore’s Schema-less NoSQL Data Model
While you don’t have to set up your own account to do the case, setting up a Firebase account, creating a project, and adding the Firestore service is pretty straightforward and they offer a quickstart tutorial.
While the data Model isn’t the only place security comes into play, it’s usually the main thing companies want to protect. The next exhibit describes Firestore’s approach to doing this with their security rules.
Exhibit D: Securing the Data Model
Like everything else in digital, security is just another type of user experience (UX). Unlike everything else in digital, your primary user is an adversary. Given that cybersecurity is kind of a thing unto itself but that application security is integrally tied to application development itself, we’ll walk through the four general steps we’ve been using to structure and focus our work.
1. Focusing Design Intent
With security, you’re always designing for two audiences: your user (making sure they can do what they need) and your attacker (making sure they can’t do what they want). You’ll find the ‘regular’ user stories for Trent the Technician in Exhibits A & B.
How about the attacker? The short answer is that hackers, mostly through software automation of their own, will attack anything and everything they can. Essentially, Cybersecurity is about understanding your stack relative to prevailing attack patterns, and then applying prevailing defense patterns to your data and infrastructure.
The good news is that a BaaS like Firebase, they’ve got a lot of the fundamentals covered for you. For example, here’s a reasonable user story for an attacker:
As an attacker, I want to find and exploit vulnerabilities in the services hosting [an] app, so I can gain privileged access to that infrastructure.
Google Cloud is running the core Firebase app and everything below it, so as long as we’re coloring inside the lines and using Firebase as intended, we can at least mostly rely on Google to take care of that for us.
However, let’s say we have this user story about our attacker:
As an attacker, I want to inject code into [an] app’s editable data so that I can direct users to fraudulent sites where I can compromise their computer and access to digital services.
Now, the Firebase SDK helps us some here by checking for conventionally bad inputs- links that run JS code, or otherwise use syntax that doesn’t match the intended purpose for the function in question. However, with our data we still need to define who we want to have what kind of access to items in our data Model. For this, we’ll wan to keep in my the user stories for our app as well as the stories and general intent of attackers. One standard approach here is to implement the principle of least privilege (PoLP), which we’ll unpack in the next section.
2. Unpacking Your Design into Codeable Steps
Starting with the fundamentals, we need to implement PoLP. The starting point for PolP is always: no one has access to anything! Then you add exceptions. Another set of terms you may have heard is that you start with a white-list exceptions for access vs. black list for restrictions.
Starting from zero access, a common approach is to initially unpack what access you need to give (read? create? update? delete?) to your various audiences in ascending order of necessary privilege:
1. Anyone, anywhere?
They can view the HVAC parts (parts collection). That’s it.
2. Validated, Authenticated User?
They can also create, edit, and delete their own reviews.
3. Administrator?
Because the team is so small and the app so early, we’ll create just one grade of administrator and give them full access to everything. However, as apps scale and mature (and need to comply with security standards like SOC-2), the team will need to get more fine-grained and particular with admin permissions.
3. Effectuating between Alternatives
How do we make this happen? Could we implement these rules in the web application (the client app)? Conceivably, but not with the way it works now since it runs on the user’s browser- even if we perform the necessary checks, a reasonably savvy user (or AI) could still formulate and issue their own requests to Firebase. Also, in terms of separation of concerns, that would mean we’d have to make sure we add and maintain those same rules if multiple places if we roll out a mobile, a partner interface, etc. Generally speaking, the prevailing approach is not to provide (or assume) security on the part of the client, but rather to do it on the server side.
Happily, Firestore offers a pretty well thought out facility for creating and testing security rules. It’s just called ‘Security Rules’ and it looks like this.
In the next section we’ll look at how to make these do what we unpacked above.
4. Iteratively Coding, Testing, and Debugging
Essentially, you’ll see three types of statements in the Firestore Rules engine:
- Functions
These are declared as functions, much like the would be in JS, and their job is to structure and organize Controller-type components. For example, there’s a function that does the job of checking whether a review is valid (isReviewValid()) and one that checks if a user is actually editing their own review (isEditingTheirOwnReview()). One notable nuance is that all the rules have to come before the spot where they called, and that’s why you see so many functions at the beginning of the screenshot above. - Record (document) matching
The ‘match’ statements tell the rules which documents path you want to define some kind of access (read, write, etc.). The beginning of the rules has the match statement ‘match /databases/{database}/documents {‘, which is essentially basically ‘all your data’ and then nested statements within it for your various Collections, like ‘match /users/{userId}’ to deal with user profile data. - Allow statements
These are kind of the punchline, these statements that, in a certain context, allow certain actions (read, write, or both). For example, this statement:
allow write: if request.auth.uid == userId; // Users can update their own profile data
under the /users/ path lets users update their profile data.
This tutorial and video from Google does a good job of providing a more general introduction: Getting started with Cloud Firestore Security Rules.
While it’s important to understand the fundamentals so you can provide the right context and check/re-prompt the results, AI is great at generating the rule sets for you. For example, I prompted one of chatGPT’s reasoning models (o3-mini-high) with a screenshot of the Firestore schema (see above), the design intent from the unpacking section (above), and I got a good set of rules.
Particularly given the nature of security, you’ll want to test the rules as well. While you’ll want to intuitively check them against y0ur design intent, here too, AI does a nice job of generating positive and negative tests, given a set of security rules. For example, here’s a set of test they generated (and I ran): chatGPT Security Rules Tests. Firestore offers a ‘Rules Playground’, which is just as fine as it sounds.
The Playground allows you to specify an operation (get, create, etc.), a path, and then, optionally, an authenticated user and also document inputs (like the rating for a review, etc.). From there, you can take the tests you’ve formulated and run them- can an admin write any record? can a user much with another user’s review?
Mostly, the environment is straightforward. One key nuance I ran into is the fact that for get and create actions, you can just specify a path and that’s most of the context the Playground needs. However, if you’re trying to simulate an update or delete operation, you’ll want to go into your actual data and use their specific paths. For example, if I want to see who can update or delete an existing review, I’ll use this actual path:
/parts/GR1688/reviews/b1KChecD1rByV0NM40vD
whereas if I want to check a general read operation I can just use /parts/part1/reviews/review1.
Exhibit E: New & Exciting Stuff in the Client JS
In terms of realizing a UX that delivers on the user stories in Exhibits A & B, a few things bear mentioning. First, there are two JS Fiddles in play:
- Parts Display Page (with Firestore)
This is the Fiddle that you’ll see from the initial sample code link above. It queries Firestore to display the various HVAC parts as well as delivering on the other stories in Exhibit A. - Parts Detail
This Fiddle is linked to from the Fiddle above. It displays detail for an individual HVAC parts and affords adding a review as well.
The subsections that follow deal with each of these pages in turn.
Parts Display Page (with Firestore)
This page displays the various HVAC parts and it allows the user to filter them. Users can also click through on an individual part for detail. Functionally, this page is similar to others you may have seen in the case library. The big new thing is that instead of providing static content or content from a flat data source (spreadsheet, etc.), it’s pulling in that data from a ‘real’ data model: Google Firestore.
First off, there’s a fair amount of code in play, just in the first Fiddle and you’ll notice multiline comments like this that delineate the major chunks of code:
// -----------------------------
// CODE THAT LOADS THE HVAC PARTS (FROM FIRESTORE)
// -----------------------------
Next, you’ll notice a pretty sizable chunk of code that chains a bunch of methods to the ‘db’ object. This is how we query data from Firestore and, since their ‘web’ SDK is so tightly integrated with JS, we can pretty fluently interleave the rest of the related Controller actions we want to realize our target UX.
Essentially, the methods chained together implement these steps:
1. reference the Firestore Collection ‘parts’
2. get the first 10 records (or whatever’s there)
3. once that’s done, iterate through each document (record), map its contents to the array partArray
4. pass that array to drawDiv, which will handle taking those inputs and creating the HVAC parts div in HTML
The DrawDiv function also calls a kind of little sister function (postReviews) that does an additional query to the reviews collection for each part to post the number of reviews and the average reviews, itself with a few helper functions to do things like draw out the star icons to denote the average review.
The rest of the code you’ll see after that is work we’ve carried over from the previous cases in the case library.
Part Detail Page
In addition to providing a larger view of the part that doesn’t require a hover action, this page allows the user to add and modify a review for the part (and, in future, versions order the part). You’ll find several functions that are the same or similar to the previous Fiddle, like postReview and displayStars. One thing this Fiddle does do that’s pretty new and exciting is that it allows the user to write data to Firestore. Specifically, it allows them to add or update a rating for an HVAC part.
Very much by design, the code that does this doesn’t look all that different than the code you saw to read the data. For example, here’s the beginning of the function, addReview, that writes new reviews to Firestore:
Basically, the set method takes the key value pairs you see in rows 8-11 and writes them to the equivalent key value pairs in that particular review (by that user, for that part).
Some Necessary Goofiness
tl;dr: Right now, the client JS code iterates through all the existing reviews to calculate the review average, but that’s not how you’d probably want to do it.
Why is it like that, then? If you guessed it’s because of the security constraints (Exhibit E), then you got it. A user is only allowed to edit their own review. A better solution (that would require geometrically less calculation) would be to maintain a review count and a review average on the part itself. Whenever a user adds or updates a review, this Controller triggers and updates the review count and adjusts the average. However, the way this code is composed (all on the client side), we’d have to let our user adjust data that they could tamper with or accidentally ‘break’, which would be pretty blatantly nonsecure.
The good news is that Firebase has a whole ‘Cloud Functions’ infrastructure to help you do this kind of ‘back end’ development. However, I felt this would add too many moving parts to the case so I undertook this ‘necessarily goofiness’.
Exhibit F: Effectuating between Model Alternatives
Creating data is like adopting a pet—you should make sure you know why you want it and be clear on how to take care of it. Of all the choices a development team will make with regard to the MVC framework, type of model will usually be the most consequential since the cost of change tends to be higher. This is primarily because changing models means moving and often transforming your data (without error), which tends to be difficult and messy.
All that said, there’s no such thing as the perfect choice here (even in retrospect), and teams need to make a reasonable decision based on what they know without an undue amount of fuss and delay. As general manager (product manager, etc.), you can bin most situations into these three archetypes:
1. We’re building a new application with a lot of unknowns and probably not a massive amount of traffic in the early days.
2. We’re building (or rebuilding) an application that will need to handle a massive amount of user activity on day one, and delivering high performance at a low cost is a major priority.
3. We’re building something for analytics—a data warehouse or data lake, which is just a somewhat less structured version of a data warehouse.
You’ll get a lot of mileage if you can just map your situation to one of these since they all have relatively obvious implications for choosing between model alternatives and you’re choosing from scratch. For #1 (a new application with many unknowns), a NoSQL alternative is probably the best bet—an open-source package like MongoDB or a cloud service like Firestore (via Google Cloud), for example. For #2 (high immediate traffic, high performance requirements), a relational database like MySQL, Oracle, or Postgres is probably the best choice. For #3 (a data lake or warehouse), an OLAP-type database is probably the best choice—a relational column store like Redis or Amazon’s Redshift, or a NoSQL OLAP option like Apache Cassandra or ElasticSearch, is probably the best way to go.
For each of these types of models, there’s also the question of how much of the solution you want to build and maintain. That said, a team with the full business context of where they are and where they’re headed with their product can substantially narrow the reasonable alternatives fairly easily. In tech, it’s conventional to think about this in terms of the “stack” a team chooses for a given application. The figure here shows a few of the most common choices.
As a technical matter, it would be more consistent to go into depth specifically on the HinH team’s options for implementing their Model; in practice, they have a much broader set of options and those are probably the right place to start for most product managers/general managers. Let’s step through those options. The software-as-a-service (SaaS) option would mean that instead of building out the application themselves, the HinH team builds it on top of another enterprise application, like Salesforce CRM. If a team finds themselves questioning the up-front expense, delay, or maintenance obligations for building their own application, this might be a good way to go.
The codeless option would mean that they build their application on top of an existing application-builder platform like Bubble, Airtable, or ServiceNow. In this case, the implementation of the model is abstracted away, and the team only needs to concern itself with the kind of high-level design you saw in the section on design intent.
The BaaS option would mean that the team has access to an abstraction layer that deals with most of the implementation of the model details—it’s not as abstract as a codeless platform, but you aren’t developing and operating nearly as much of the model’s infrastructure. Finally, we have the option of platform-as-a-service (PaaS), or building out an entire custom app.
Google’s Firebase is an example of a NoSQL-based BaaS, and that’s essentially why the HinH team ended up there in this particular case. In order to ask better questions and facilitate better discussions around model choice, it’s useful to understand some of the model-related questions that your development colleagues are likely to be concerned with. Here are a few of the most typical:
1. How do I design the model in a way that maximizes what I know but is also amenable to change, given the disruptive nature of database schema changes?
2. How do I make the model available to our developers in a way that minimizes overly tight coupling, where small changes to the database break a lot of the code?
3. How do I maximize the performance of the model for good UX at a reasonable cost?
4. How do I maintain the uptime and availability of the model?
5. How do I secure the model?
Acknowledgements
I’d like to thank Philip Halsey for his excellent contributions on this case, and also all the course alumni who shaped the approach to this case.