Firebase Crash Course

Avatar of David East
David East on

This article is going to help you, dear front-end developer, understand all that is Firebase. We’re going to cover lots of details about what Firebase is, why it can be useful to you, and show examples of how. But first, I think you’ll enjoy a little story about how Firebase came to be.

Eight years ago, Andrew Lee and James Tamplin were building a real-time chat startup called Envolve. The service was a hit. It was used on sites of celebrities like Ricky Martin and Limp Bizkit. Envolve was for developers who didn’t want to—yet again—build their own chat widget. The value of the service came from the setup ease and speed of message delivery. Envolve was just a chat widget. The process was simple. Place a script tag on a page. Once the widget booted up, it would do everything for you. It was like having a database and server for chat messages already set up.

James and Andrew noticed a peculiar trend as the service became more popular. Some developers would include the widget on the page, but make it invisible. Why would someone want a chat widget with hidden messages? Well, it wasn’t just chat data being sent between devices. It was game data, high scores, app settings, to-dos, or whatever the developer needed to quickly send and synchronize. Developers would listen for new messages in the widget and use them to synchronize state in their apps. This was an easy way to create real-time experiences without the need for a back end.

This was a light bulb moment for the co-founders of Firebase. What if developers could do more than send chat messages? What if they had a service to quickly develop and scale their applications? Remove the responsibility of managing back-end infrastructure. Focus on the front end. This is how Firebase was born.

What is Firebase?

Isn’t Firebase a database? No… yes… mostly no. Firebase is a platform that provides the infrastructure for developers and tools for marketers. But it wasn’t always that way.

Seven years ago, Firebase was a single product: a real-time cloud database. Today, Firebase is a collection of 19 products. Each product is designed to empower a part of an application’s infrastructure. Firebase also gives you insight into how your app is performing, what your users are doing, and how you can make your overall app experience better. While Firebase can make up the entirety of your app’s back-end, you can use each product individually as well.

Here’s just a sampling of those 19 products:

  • Hosting: Deploy a new version of your site for every GitHub pull request.
  • Firestore: Build apps that work in real time, even while offline, with no server required.
  • Auth: Authenticate and manage users with a myriad of providers.
  • Storage: Manage user-generated content like photos, videos, and GIFs.
  • Cloud Functions: Server code driven by events (e.g. record created, user sign up, etc.).
  • Extensions: Pre-packaged functions set up with UI (e.g. Stripe payments, text translations, etc.)
  • Google Analytics: Understand user activity, organized by segments and audiences.
  • Remote Config: Key-value store with dynamic conditions that’s great for feature gating.
  • Performance Monitoring: Page load metrics and custom traces from real usage.
  • Cloud Messaging: Cross-platform push notifications.

Whew. That’s a lot, and I didn’t even list the other nine products. That’s okay. There’s no requirement to use every specific service or even more than one thing. But now it’s time to make these services a little more tangible and showcase what you can do with Firebase.

A great way to learn is by seeing something just work. The first section below will get you set up with Firebase services. The sections following that will highlight the Firebase details of a demo app to showcase how to use Firebase features. While this is a relatively thorough guide to Firebase, it’s not a step-by-step tutorial. The goal is to highlight the working bits in the embedded demos for the sake of covering more ground in this one article. If you want step-by-step Firebase tutorials, leave a comment to hype me up to write one.

A basic Firebase setup

This section is helpful if you plan on forking the demo with your own Firebase back end. You can skip this section if you’re familiar with Firebase Projects or just want to see the shiny demos.

Firebase is a cloud-based service which means you need to do some basic account setup before using its services. Firebase development is not tied to a network connection, however. It’s very much worth noting that you can (and usually should) run Firebase locally on your machine for development. This guide demonstrates building an app with CodePen, which means it needs a cloud-connected service. The goal here is to create your personal back end with Firebase and then retrieve the configuration the front end needs to connect to it.

Create a Firebase Project

Go to the Firebase Console. You’ll be asked if you want to set up Google Analytics. None of these examples use it, so you can skip it and always add it back in later if needed.

Create a web Firebase App

Next, you’ll see options for creating an “App.” Click the web option and give it a name—any name will do. Firebase Projects can hold multiple “apps.” I’m not going to get deep into this hierarchy because it’s not too important when getting started. Once the app is created you’ll be given a configuration object.

let firebaseConfig = {
  apiKey: "your-key",
  authDomain: "your-domain.firebaseapp.com",
  projectId: "your-projectId",
  storageBucket: "your-projectId.appspot.com",
  messagingSenderId: "your-senderId",
  appId: "your-appId",
  measurementId: "your-measurementId"
};

This is the configuration you’ll use on the front end to connect to Firebase. Don’t worry about any of these properties in terms of security. There’s nothing insecure about including these properties in your front-end code. You’ll learn in one of the sections below how security works in Firebase.

Now it’s time to represent this “App” you created in code. This “app” is merely a container that shares logic and authentication state across different Firebase services. Firebase provides a set of libraries that make development a lot easier. In this example I’ll be using them from a CDN, but they also work well with module bundlers like Webpack and Rollup.

// This pen adds Firebase via the "Add External Scripts" option in codepen
// https://www.gstatic.com/firebasejs/8.2.10/firebase-app.js
// https://www.gstatic.com/firebasejs/8.2.10/firebase-auth.js

// Create a Project at the Firebase Console
// (console.firebase.google.com)
let firebaseConfig = {
  apiKey: "your-key",
  authDomain: "your-domain.firebaseapp.com",
  projectId: "your-projectId",
  storageBucket: "your-projectId.appspot.com",
  messagingSenderId: "your-senderId",
  appId: "your-appId",
  measurementId: "your-measurementId"
};

// Create your Firebase app
let firebaseApp = firebase.initializeApp(firebaseConfig);
// The auth instance
console.log(firebaseApp.auth());

Great! You are so, so close to being able to talk to your very own Firebase back end. Now you need to enable the services you intend to use.

Enable authentication providers

The examples below use authentication to sign in users and secure data in the database. When you create a new Firebase Project, all of your authentication providers are turned off. This is initially inconvenient, but essential for security. You don’t want users trying to sign in with providers your back end does not support.

To turn on a provider go to the Authentication tab in the side navigation and then click the “Sign-in method” button up top. Below, you’ll see a large list of providers such as Email and Password, Google, Facebook, GitHub, Microsoft, and Twitter. For the examples below, you will need to turn on Google and Anonymous. Google is near the top of the list and Anonymous is at the bottom. Selecting Google will ask you to provide a support email, I recommend putting in your own personal email while testing, but production apps should have a dedicated email.

If you plan on using authentication within CodePen, then you’ll also need to add CodePen as an authorized domain. You can add authorized domains towards the bottom of the “Sign-in method” tab.

An important note on this authorization: this will allow any project hosted on cdpn.io to sign into your Firebase Project. There’s not a lot of risk for short-term demo purposes. There’s no cost to using Firebase Auth except for phone number authentication. Ideally, you would not want to keep this as an authorized domain if you plan on using this app in a production capacity.

Now, on to the last step in the Firebase Console: Creating a Firestore database!

Create a Firestore database

Click on Firestore in the left navigation. From here, you’ll need to click the button to create the Firestore database. You’ll be asked if you want to start in “production mode” or “test mode.” You want “test mode” for this example. If you’re worried about security, we’ll cover that in the last section.

Now that you have the basics down, let’s get to some real-life use cases.

Authenticating users

The best parts of an app are usually behind a sign-up form. Why don’t we just let the user in as a guest so they can see for themselves? Systems often require accounts because the back end isn’t designed for guests. There’s a system requirement to have a sacred userId property to save any record belonging to a user. This is where “guest” accounts are helpful. They provide low friction for users to join while giving them a temporary userId that appeases the system. Firebase Auth has a process for doing just this.

Setting up anonymous auth

One of my favorite features about Firebase Auth is anonymous auth. There are two perks. One, you can authenticate a user without taking any input (e.g. passwords, phone number, etc.). Two, you get really good at spelling anonymous. It’s really a win-win situation.

Take this CodePen for example.

It’s a form that lets the user decide if they want to sign in with Google or as a guest. Most of the code in this example is specific to the user interface. The Firebase bits are actually pretty easy.

// Firebase-specific code
let firebaseConfig = { /* config */ };
let firebaseApp = firebase.initializeApp(firebaseConfig);
// End Firebase-specific code
let socialForm = document.querySelector('form.sign-in-social');
let guestForm = document.querySelector('form.sign-in-guest');
guestForm.addEventListener('submit', async submitEvent => {
  submitEvent.preventDefault();
  let formData = new FormData(guestForm);
  let displayName = formData.get('name');
  let photoURL = await getRandomPhotoURL();
  // Firebase-specific code
  let { user } = await firebaseApp.auth().signInAnonymously();
  await user.updateProfile({ displayName, photoURL });
  // End Firebase-specific code
});

In the 17 lines of code above (sans comments), only five of them are Firebase-specific. Four lines are needed to import and configure. Two lines needed to signInAnonymously() and user.updateProfile(). The first sign-in method makes a call to your Firebase back end and authenticates the user. The call returns a result that contains the needed properties, such as the uid. Even with guest users, you can associate data to a user in your back end with this uid. After the user is signed in, the example calls updateProfile on the user object. Even though this user is a guest, they can still have a display name and a profile photo.

Setting up Google auth

The great news is that this works the same exact way with all other permanent providers, like Email and Password, Google, Facebook, Twitter, GitHub, Microsoft, Phone Number, and so much more. Implementing the Google Sign-in only takes a few lines of code as well.

socialForm.addEventListener('submit', submitEvent => {
  submitEvent.preventDefault();

  // Firebase-specific code
  let provider = new firebase.auth.GoogleAuthProvider();
  firebaseApp.auth().signInWithRedirect(provider);
  // End Firebase-specific code
});

Each social style provider initiates a redirect-based authentication flow. That’s a fancy way of saying that the signInWithRedirect method will go to the sign-in page owned by the provider and then return back to your app with the authenticated user. In this case, the user is redirected to Google’s sign-in page, signs in, and then is returned back to your app.

Monitoring authentication state

How do you get the user back from this redirect? There are a few ways, but I’m going to go with the most common. You can detect the authentication state of any active user whether logged in or out.

firebaseApp.auth().onAuthStateChanged(user => {
  if(user != null) {
    console.log(user.toJSON());
  } else {
    console.log("No user!");
  }
});

The onAuthStateChanged method updates whenever there is a change in a user’s authentication state. It will fire initially on page load telling you if a user is logged in, logs in, or logs out. This allows you to build a UI that reacts to these state changes. It also fits well into client-side routers because it can redirect a user to the proper page with a small amount of code.

The app in this case just uses <template> tags to replace the contents of the “phone” element. If the user is logged in, the app routes to the new template.

<div class="container">
  <div class="phone">
    <!-- Phone contents replaced with template tags -->
  </div>
</div>

This provides a simple relationship. The .phone is the “root view.” Each <template> tag is a “child view.” The authentication state determines which view is shown.

firebaseApp.auth().onAuthStateChanged(user => {
  if(user != null) {
    // Show demo view
    routeTo("demo", firebaseApp, user);
  } else {
    console.log("No user!");
    // Show log in page
    routeTo("signIn", firebaseApp);
  }
});

The embedded demo isn’t “production ready” as the requirements were just work inside a single CodePen. The goal was to make it easy to read with each “view” contained within a template tag. This loosely emulates what a common routing solution would look like with a framework.

One important bit to note here is that the user object is passed down to the routeTo function. Getting the logged-in state is asynchronous. I find that it’s much easier to pass the user state down for the view than to make async calls from within the view. Many framework routers have a spot for this kind of async data fetching.

Convert guests to permanent users

The <template id="demo"> tag has a submit button for converting guests to permanent users. This is done by having the user sign in with the main provider (again, Google in this case).

let convertForm = document.querySelector('form.convert');
convertForm.addEventListener("submit", submitEvent => {
  submitEvent.preventDefault();
  let provider = new firebase.auth.GoogleAuthProvider();
  firebaseApp.auth().currentUser.linkWithRedirect(provider);
});

Using the linkWithRedirect method will kick the user out to authenticate with the social provider. When the redirect returns the account will be merged. There’s nothing you have to change within the onAuthStateChanged method that controls the “child view” routing. What’s important to note is that the uid remains the same.

Handling account merging errors

A few edge cases can pop-up when you merge accounts. In some cases, a user could already have created an account with Google and is now trying to merge that existing account. There are many ways you can handle this scenario depending on how you want your app to work. I’m not going to get deep into this because we have a lot more to cover, but it’s important to know how to handle these errors.

async function checkForRedirect() {
  let auth = firebaseApp.auth();
  try {
    let result = await auth.getRedirectResult();
  }
  catch (error) {
    switch(error.code) {
      case 'auth/credential-already-in-use': {
        // You can check for the provider(s) in use
        let providers = await auth.fetchProvidersForEmail(error.email);
        // Then decide what strategy to take. A possible strategy is
        // notifying the user and asking them to sign in as that account
      }
    }
  }
}

The code above uses the getRedirectResult method to detect if a user has directly returned from a social login redirect. If so, there’s a lot of information in that result. Most importantly here, we want to know if there was a problem. Firebase Auth will throw an error and provide relevant information on the error, such as the email and credentials, which will allow you to continue to merge the account. That’s not always what you want to do. In this case, I would probably indicate to the user that the account exists and prompt them to sign in with it. But I digress; I could talk sign-in forms for ages.

Now it’s on to building the data visualization (okay, it’s just a pie-chart) and providing it with a realtime data stream.

Setting up the data visualization

I’ll be honest. I have no idea what this app really does. I really wanted to test out building pie charts with a conic-gradient. I was excited about using CSS Custom Properties to change the values of the chart and syncing that in real time with a database, like Firestore. Let’s take a brief little detour to discuss how conic-gradient works.

A conic-gradient is a surprisingly well-supported CSS feature. Its key feature is that its color-stops are placed at the circumference of the circle.

.pie-chart {
  background-image: conic-gradient(
     purple 10%, /* 10% of the circumference */
     magenta 0 20%, /* start at 0 go 20%, acts like 10% */
     cyan 0 /* Fill the rest */
  );  
}

You can build a pie chart with some quick math: lastStopPercent + percent. I stored these values in four CSS Custom Properties in the app pen: --pie-{n}-value (replace n with a number). Those values are used in another custom property that serves like a computed function.

:root {
  --pie-1-value: 10%;
  --pie-2-value: 10%;
  --pie-3-value: 80%;

  --pie-1-computed: var(--pie-1-value);
  --pie-2-computed: 0 calc(var(--pie-1-value) + var(--pie-2-value));
  --pie-3-computed: 0 calc(var(--pie-2-value) + var(--pie-3-value));
}

Then the computed values are set in the conic-gradient.

background-image: conic-gradient(
  purple var(--pie-1-computed),
  magenta var(--pie-2-computed),
  cyan 0
);

The last computed value, --pie-3-computed, is ignored since it will always fill to the end (kinda like z for SVG paths). I think it’s still a good idea to set it in JavaScript to make the whole thing feel like it makes sense.

function setPieChartValue(percentage, index) {
  let root = document.documentElement;
  root.style.setProperty(`--pie-${index+1}-value`, `${percentage}%`);
}

let percentages = [25, 35, 60];
percentages.forEach(setPieChartValue);

You can hook up with pie chart with any data set with this newfound knowledge. The Firestore database has a .onSnapshot() method that streams data back from your database.

const fullPathDoc = firebaseApp.firestore().doc('/users/1234/expenses/3-2021');
fullPathDoc.onSnapshot(snap => {
  const { items } = doc.data();
  items.forEach(setPieChartValue);
});

A real time update will trigger the .onSnapshot() method whenever a value changes in your Firestore database at that document location. Now you might be asking yourself, what is a document location? How do I even store data in this database in the first place? Let’s take a dive into how Firestore works and how to model data in NoSQL databases.

How to model data in NoSQL database

Firestore is a document (NoSQL) database. It provides a hierarchical pattern of collections, which is a list of documents. Think of a document like a JSON object that has many more data types. A document can have a collection itself, which is known as a sub-collection. This is good for structuring data in a “parent-child” or hierarchical pattern.

If you haven’t followed the pre-requisites section up top, give it a read to make sure you have a Firestore database created before using any of this code yourself.

Just like Auth, you first need to import Firestore up top.

// This pen adds Firebase via the "Add External Scripts" option in codepen
// https://www.gstatic.com/firebasejs/8.2.10/firebase-app.js
// https://www.gstatic.com/firebasejs/8.2.10/firebase-auth.js
// https://www.gstatic.com/firebasejs/8.2.10/firebase-firestore.js

let firebaseConfig = { /* config */ };
let firebaseApp = firebase.initializeApp(firebaseConfig);

This tells the Firebase client library how to connect your Firestore database. Using this setup, you can create a reference to a piece of data in your database.

// Reference to collection stored at: '/expenses'
const expensesCol = firebaseApp.firestore('expenses');

// Retrieve a snapshot of data (not in realtime)
// Top level await is coming! (v8.dev/features/top-level-await)
const snapshot = await expensesCol.get();
const expenses = snapshot.docs.map(d => d.data());

The code above creates a “Collection Reference” and then calls the .get() method to retrieve the data snapshot. This is not the data itself; it’s a wrapper that has a lot of helpful methods and metadata about the data. The last line “unwraps” the snapshot by iterating over the .docs array and calling the .data() function for each “Document Snapshot.” Note that this isn’t real time, but that’s coming up in just a bit!

What’s really important to grok here is the data structure (sometimes called a data model). This app stores the “expenses” of a user. Let’s say the document has the following structure.

{
  uid: '1234',
  items: [
    { label: "Food", value: 10 },
    { label: "Services", value: 24 },
    { label: "Rent", value: 30 },
    { label: "Oops", value: 38 }
  ]
}

The properties of a document are called a field. This document has an array field of items. Each item contains a label string and a value number. There’s also a string field named uid that stores the id of the user who owns the data. This structure simplifies the process of iterating over the values to create the pie chart. That part is solved, but how do we figure out how to get to a specific user’s expenses?

A traditional way of retrieving data based on a constraint is by using a query. That’s something you can do in Firestore with a .where() method.

// Let's pretend currentUser.uid === '1234'
const currentUser = firebaseApp.auth().currentUser;

// Reference to collection stored at: '/expenses'
const expensesCol = firebaseApp.firestore('expenses');

// Query for the expenses belonging to uid == 1234
const userQuery = expensesCol.where('uid', '==', currentUser.uid);
const snapshot = await userQuery.get();

This structure is fine but doesn’t take full advantage of the hierarchy provided by collections and, more importantly, sub-collections. Instead, you can structure your data in a “parent-child” relationship. Structures in Firestore work a lot like URLs for a website. You can design the paths to contain route parameter-like wildcards.

/users/:uid/expenses/month-year

The path above structures the data with a top-level collection of /users, then a document assigned to a uid, followed by a sub-collection. Sub-collections can exist even if there’s no existing parent document. Each sub-collection contains a document where you can retrieve the expenses by putting in the year as a number (e.g. 3 for March) and the year (2021).

// Let's pretend currentUser.uid === '1234'
const currentUser = firebaseApp.auth().currentUser;

// Reference to collection stored at: '/users'
const usersCol = firebaseApp.firestore().collection('users');

// Reference to document stored at: '/users/1234';
const userDoc = usersCol.doc(currentUser.uid);

// Reference to sub-collection stored at: '/users/1234/expenses'
const userExpensesCol = userDoc.collection('expenses');

// Reference to document stored at: '/users/1234/expenses/3-2021';
const marchDoc = userExpensesCol.doc('3-2021');

// Alternatively, you could express a full path:
const fullPathDoc = firebaseApp.firestore().doc('/users/1234/expenses/3-2021');

The example above shows that, with a little bit of modeling, you can retrieve a piece of data without needing to write a query. This won’t always be the case for each and every data structure. In many cases, it will be valid to have a top-level collection. It comes down to the kind of queries you want to write and how you want to secure your data. It’s good, however, to know your options when modeling the data. If you want to learn more about this topic, my colleague Todd Kerpelman has recorded a comprehensive series all about Firestore.

We’re going to go with the hierarchical approach in this app. With this data structured let’s stream in real time.

Streaming data to the visualization

The section above detail how to retrieve data in a “one-time” manner with .get(). Both documents and collections also have a .onSnapshot() method that allows you to stream the data in realtime.

const fullPathDoc = firebaseApp.firestore().doc('/users/1234/expenses/3-2021');
fullPathDoc.onSnapshot(snap => {
  const { items } = doc.data();
  items.forEach((item, index) => {
    console.log(item);
  });
});

Whenever an update happens for the data stored at fullPathDoc, Firestore will stream the update to all connected clients. Using this data sync you can set all the CSS Custom Properties for the pie chart.

let db = firebaseApp.firestore();
let { uid } = firebaseApp.auth().currentUser;
let marchDoc = db.doc(`users/${uid}/expenses/3-2021`);
marchDoc.onSnapshot(snap => {
  let { factors } = snap.data();
  factors.forEach((factor, index) => {
    root.style.setProperty(`--pie-${index+1}__value`, `${factor.value}%`);
  });    
});

Now for the fun part! Go update the data in the database and see the pie slices move around.

Honestly, that is so much fun. I’ve been working on Firebase for nearly seven years and I never get tired of seeing the lightning-fast data updates. But the job is not yet complete! The database is insecure as it can be updated by anyone anywhere. It’s time to make sure it’s secure only to the user who owns that data.

Securing your database

If you’re new to Firebase or back-end development, you might be wondering why the database is insecure at the current moment. Firebase allows you to build apps without running your own server. This means you can connect to a database directly from the browser. So, if it’s that easy to access the database, what keeps someone from coming along and doing something malicious? The answer is Firebase Security Rules.

Security Rules are how you secure access to your Firebase services. You write a set of rules that specify how data is accessed in your back end. Firebase evaluates these rules for each request that comes into your back end and only allows the request if it passes the rule. In other words, you write a set of rules and Firebase runs them on the server to secure access to your services.

Security Rules are like a router

Security Rules are a custom language that works a lot like a router.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // When a request comes in for a "user/:userId"
    // let's allow the read or write (not very secure)
    // Don't copy this in your code plz!
    match /users/{userId} {
      allow read, write: if userId == "david";
    }
  }
}

The example above starts with a rules_version statement. Don’t worry too much about that, but it’s how you tell Firebase the version of the Security Rules language you want to use. Currently, it’s recommended to use version 2. Then the example goes on to create a service declaration. This tells Firebase what service you are trying to secure, which is Firestore in this case. Now, on to the important part: the match statements.

A match statement is the part that makes rules work like a router. The first match statement in the example establishes that you are matching for documents in the database. It’s a very general statement that says, Hey, look in the documents section. The next match statement is more specific. It looks to match a document within the users collection. The path syntax of /users/{userId} is similar to a routing syntax of /users/:userId where the :userId syntax notes a route parameter. In Security Rules, the {userId} syntax works just like route parameter, except that it’s called “wildcard” here. Any user within the collection can be matched with this statement. What happens when the user is matched? You use the allow statement to control the access.

The allow statement evaluates an expression and, if the result is true, it allows the operation. If it’s false, the operation is rejected. What’s useful about the allow statement is that there’s a lot of useful information to use in the containing match block. One useful piece of information is the wildcard itself. The example above uses the userId wildcard like a variable and tests to see if it matches the value of "``david``". Only a userId of "``david``" will allow the read or write operation.

Now that’s not a very useful rule, but it helps to start simple. Let’s take a moment to remember the structure of the database. Security Rules are a lot like an annotation on top of your data structure. You can make notes at the document paths that enforce your data access. This app stores data at a /users/{userId} collection and expenses at a /users/{userId}/expenses/month-year sub-collection. The security strategy is to ensure that only the authenticated user can read or write their data.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId}/{documents=**} {
      allow read, write: if request.auth.uid == userId;
    }
  }
}

The example above starts off the same but starts to change when matching the /users/{userId} path. There’s this weird {documents=**} syntax tacked on at the end. This is called the recursive wildcard and it’s a way of cascading a rule to sub-collections—meaning any sub-collection of /users/{userId} will have the same rules applied to it. This is great in the current use case because both /users/{userId} and /users/{userId}/expenses/month-year should follow the same rule.

Inside of that match, the allow statement has been updated with a new variable named request. Security Rules come with an entire set of variables to help you write sophisticated rules. This variable is how you evaluate if the request comes from an authenticated user. The allow statement evaluates if the authenticated user has a uid that matches the {userId} wildcard. If that statement evaluates to true, the read or write is allowed. If the user is not authenticated or does not match the {userId} wildcard, no operation is allowed. Therefore, it’s secure!

The following snippet shows two requests from an authenticated user.

// Let's pretend currentUser.uid === '1234'
const currentUser = firebaseApp.auth().currentUser;

// The authenticated user owns this sub-collection
const ownedDoc = firebaseApp.firestore().doc('/users/1234/expenses/3-2021');

// The authenticated user DOES NOT own this sub-collection 
const notOwnedDoc = firebaseApp.firestore().doc('/users/abcxyz/expenses/3-2021');

try {
  const ownedSnapshot = await ownedDoc.get();
  const notOwnedSnapshot = await notOwnedDoc.get();
} catch (error) {
  // This will result in an error because the `notOwnedDoc` request will fail the security rule
}

Just like that, you have secured a collection and a sub-collection. But what about the rest of the data in the database? What happens if you don’t write any rules for data at those paths? The example above will only allow access that matches the allow statement for the /users collection and its sub-collections. No other reads or writes will work, at all, for any other collections or sub-collections. In other words, if you don’t write an allow statement for a path, then it won’t allow any reads or writes. This is a great default for security because it makes you explicitly enforce your access at the appropriate paths. You can mess this up, however!

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Ahh!!! Never do this in a production app!!!
    // This will negate any rule you write below!!
    // Don't copy and paste this into your rules!!
    match /{document=**} {
      allow read, write: if true;
    }
    match /users/{userId}/{documents=**} {
      allow read, write: if request.auth.uid == userId;
    }
  }
}

The sample above adds a match block that matches the path of /{document=**}. This is a recursive wildcard path that matches every document in the entire database. The allow statement always evaluates to true because it is true. This match block creates an overlapping match statement, meaning two or more match blocks that match on the same path. This isn’t like CSS where the last rule wins. Security Rules will evaluate each rule matched and if any of them evaluate to true, the operation is allowed. Therefore, the global recursive wildcard will negate any secure rule you have below. The global recursive wildcard is a strategy for opening up your database for a short-term “test mode” where there is no important non-publicly accessible data (nothing private or important) saved. Outside of that, I don’t recommend using it.

Write rules locally or in the console

The final topic to touch upon is where you write and save your rules. You have two options. The first is inside of the Firebase Console. Within the Firestore data viewer you’ll see an option for “Rules.” This tab shows you the rules that are active for your database. From here, you can write and even test scenarios against your rules. This approach is recommended for those who are getting started and trying to become familiar with Security Rules.

Another option is write rules locally on your machine and use the Firebase CLI to deploy them to the console. This allows you to keep them in source control and write tests for them to make sure they continue to work as your codebase evolves. This is the recommended approach for production apps and teams.

It’s worth noting again that your Firebase configuration used to create a Firebase App is not insecure. It’s the equivalent of someone knowing the domain name of your site. Someone knowing your domain name doesn’t make your site insecure. Security Rules are Firebase’s way of providing secure access to your data and your services.

Wrapping things up

That was a lot of Firebase information, especially about all the stuff about rules (security is important!). The information in this article signs in users, merges guest accounts, structures data, streams it to a visualization in real time, and makes sure it’s all secure. You can use these concepts to build so many different applications.

There are so many things I want you to know if you want to continue building with Firebase, such as the Emulator Suite. The app built in this article can run locally on your machine, which is a far easier development experience and great for testing in CI/CD environments. There are also a lot of great Firebase tools and framework libraries. Here are some links worth checking out.

If there’s one thing I hope you saw in this article, it’s that there aren’t too many lines of front-end Firebase code. A line here to sign in a user, a few lines there to get the data, but mostly code that’s specific to the app itself. The goal of Firebase is to allow you to build quickly. Remove the responsibility of managing backend infrastructure. Focus on the front end.