When You Should (and Shouldn't) Use Firebase

Published Jan 04, 2018Last updated Apr 24, 2018
When You Should (and Shouldn't) Use Firebase

I first used Firebase for a hackathon (AngelHack to be specific) back in 2014. It certainly had its advantages. It was quick to set up. Authentication was included out of the box and NoSQL was appealing to me, having cut my teeth on all of the forethought one had to put into schema design with PostgreSQL.

You never really know a technology until the honeymoon is over and you are familiar with all of the ways it sucks. Don't get me wrong, for the right use cases, Firebase is an excellent tool. However, in the course of my consulting career, I've seen plenty of clients without the technical background request Firebase for their MVPs without really considering if it's the right tool for their needs.

While I will list some guidelines, I would like to admonish my non-technical audience that it is in your best interest to choose the best tech lead for the job and explain to them the use cases and requirements. It's their expertise to translate that into the best technology choices for the job.

What firebase does

Firebase, in this article, relates to a set of tools offered by Google to build scalable applications in the cloud. The core product is their realtime database. Not to be confused with their production realtime database, Firebase also has a Cloud Firestore product in beta which purports to address some of the limitations I will be pointing out in their realtime database. While it looks like a great piece of technology, it is still too new for anyone to be building production systems on.

Firebase in a nutshell

Firebase is best thought of as a persistent hash datastructure in the cloud. For the layman, think of a Firebase database as elements in a set where you can retrieve or set values via a key. Abstractly, you can think of Firebase like a JSON object.

// not actual firebase code
let db = firebase(config)
db.set('property', value)
db.get('property') // => value

Seamless synchronization is the point and almost the whole point

The main benefit of the realtime database is that your data is synchronized across all of your clients. In practice, this means that we have to deviate a bit from the initial conceptional model of our React database as a simple JSON. While setting a property is similar, you don't simply get a value with the property. Instead, you subscribe to a channel for changes to that property. With that in mind, you design a Firebase client differently than you would with a traditional REST API.

A typical REST API interaction involves:

  1. controller determines what endpoints to hit and with what parameters.
  2. controller makes the API calls.
  3. controller updates the views with the pertinent data.
controller.on('load', () => $.post(params)
  .then((data) => {
   		this.set('store', data)
        this.refreshView()
   	}));

Responsibility for getting fresh data is delegated to the controller. At its heart, this is an imperative view of the world. It is the developer's responsibility to continuously pull data from the endpoint and merge in changes.

One common anti-pattern I've observed in Firebase clients is to treat it with the same lifecycle as a REST API using .once().

This code is a concrete example, but feel free to skip if you are non-technical.

var rootRef = firebase.database().ref("https://<DATABASE_NAME>.firebaseio.com");

...
//react like example
class PetController extends Component {
  constructor(props) {
      super(props);
      this.firePath = rootRef.child(`/pets/${props.id}`)
      this.state = {
      	pet: {}
      };
     //...
    }
    fetchPetData() {
      this.firePath.once('value')
      .then((snapshot) => {
     this.setState({ pet: snapshot.val()});
      });
    }
    onLoad() {
      fetchPetData();
    }
    render() {
       let pet = this.state.pet
       return (
       	<div>
        	<h1>{pet.name}</h1>
            <ul>
              <li>{pet.age}</li>
            </ul>
        </div>
       );
    }
}

As a general rule, this is the wrong approach. The controller has to explicitly rerun the fetchPetData() method in order to avoid stale data. With rare exceptions, if you are accessing data this way, you're losing out on the main advantages of Firebase.

A better way is to set up listeners on initialization and have it run the update as new data comes in.

// declarative version
class PetController extends Component {
  constructor(props) {
      super(props);
      this.firePath = rootRef.child(`/pets/${props.id}`)
      this.state = {
      	pet: {}
      };
    }
    componentDidMount() {
      let subscription = this.firePath.on('value'(snapshot) => {
     this.setState({ pet: snapshot.val()});
      });
      this.setState({subscription});
    }
    componentWillUnmount() {
      // unsubscribe or you will have memory leaks
      this.state.subscription.off();
    }
    render() {
       let pet = this.state.pet
       return (
       	<div>
        	<h1>{pet.name}</h1>
            <ul>
              <li>{pet.age}</li>
            </ul>
        </div>
       );
    }
}

To break it down, instead of explicitly calling the pet data, we set up a listener on Firebase. Firebase then sends down the data both on initialization as well as any time that data gets changed on the server. Simply updating the property via set will update the property on the server, which will send down the updated value.

The key difference of note is that we declare the UI as a product of the subscription of changes on the element and react to them. If this lifecycle does not match your use case, Firebase is the wrong tool for the job.

Shortcomings

As stated, Firebase's main benefit is the ability to react to changes on your collections and elements. Doing this in a fast, scalable realtime way means that Firebase has to impose a few constraints.

Queries are Shit

If you need deep querying, (i.e., find me all pets over two years old that are Cocker Spaniels or Laboradors), you're out of luck. Seriously, if this if something you need, stay away from Firebase.

You can't even do something relatively simple, like reversing the order of elements in a collection as they come down. At best, you can apply constraints to ONE property and order by it. This makes sense from the subscription of changes point of view, but not for most use cases.

One workaround/hack I've seen is to download the whole dataset and reverse it locally. This gets really complicated if your list is very big, and again, is going completely against the grain of Firebase.

To reiterate, if you need to make any kind of query more complicated than "find all dogs older than x AND order by age," you're SOL trying to make that work in Firebase. Just about anything is going to better than Firebase.

No relational queries

Relational databases have endured for a reason. Most data that we work with is deeply relational. Firebase relies on a very flat hierarchy of nested data. The recommended advice for relational data is to have duplicate data for each element. Of course, that means you need to have have some way to sync data when its changed in any element.

{
  pets: {
  	asdfkasgfdk: {
      name: 'fido',
      gender: 'male',
      type: 'dog'
      breed: {
       name: 'cockerspaniel'
       hairType: 'long'
      }
    },
    asdfkasgfdk: {
      name: 'fifi',
      type: 'dog',
      gender: 'female',
      breed: {
       name: 'cockerspaniel'
       hairType: 'long'
      }
    }
  }
}

You can either query all of the elements that have the same sub-element (and you know how Firebase queries are now) or store an ID and have it somewhere else, necessitating setting up two different listeners and performing the join yourself on the client. Neither is going to be NICE by any sense of the word.

I've made this work for simple relations, but if your data model needs anything more than a single join, stay away from Firebase.

Where Firebase is awesome

Given the downsides of Firebase, It would be disingenuous to recommend it as a primary database. I have found a few use cases where it is a great tool for the job.

Cloud Based Event Queue

While RabbitMQ or Redis would be the gold standard for background jobs, you can have a completely serverless background job setup using Firebase with Google Cloud functions.

See: extending with functions

Screen Shot 2017-12-31 at 8.24.20 PM.png

The major advantage here is that you can designate a collection in Firebase with a particular schema and authorization access, and queue background jobs with Firebase directly. You can then set a cloud function to trigger on creation, perform some task, and update the queue item with the status of the transaction.

exports.createBatchExport = functions.database.ref('/queue/{pushId}')
.onCreate(event => {
  const original = event.data.val();
  const object = event.data;
  doSomething(event)
    .then((result) => {
      return event.data.ref.update({
        result: result,
        status: 'ok'
      });
    })
    .catch((err) => {
      return event.data.ref.update({
      error: err,
          status: 'error'
        });
    })
});

This kind of setup is dead simple and your costs go up with increased usage, so it's great for an MVP.

Realtime Notifications

Most frameworks, like Rails and Node, have websocket functionality for realtime push data. However, deployment can be tricky. It can be a full-time job scaling actionable in production. A much simpler solution is to have each user be given a collection of notifications. The server can push to it with REST calls to Firebase and have the user recieve them over Firebase. This is easy because the collection could be modified directly by the user. The most complicated query required for this setup is a limit to the most recent unread notifcation. It's simple and will scale.

Realtime Chat/messaging

This is a pretty easy sell — you have a collection of rooms and each one is a set of messages.

Synched Application State

Certain preferences may need to be shared across multiple clients. Things like recent search history, most recently viewed items, or a shopping cart come to mind. In this case, Firebase is great because it's just a bucket of data that can be allocated for each user in a collection of users. Simply have the user subscribe to their particlar bucket.

In Conclusion

Firebase's realtime database is a very powerful tool for a limited scope. To know if your data is a good match for Firebase, simply ask yourself if you'd want to use a observable hash. If all you need is to react to the addition/update of items in a collection or object, Firebase is great. If you need extensive queries or have complex relational data, Firebase would be a poor choice for your main database. Still, it could still be an excellent component of a well balanced serverside infrastructure.

Discover and read more posts from Peter de Croos
get started