@Calendee

API Versioning Trick for Firebase

Many people make the mistake of thinking that Firebase is simply a way to sync data between devices. Without digging any deeper, you might think it's a bit of a one-trick pony. Anyone that's ever seen a demonstration of Firebase, will know that Firebase is way more than this. Firebase can be your complete API, authentication system, and database.

Replacing a typical LAMP or "MEAN" stack API with Firebase does present some challenges. A developer really has to change their mindset about how the client interacts with the API. Firebase security and validation is all done with a unique JSON based rules system that you'll have to spend some time wrapping your head around. There's no REST API convention with Firebase. You can do pretty much anything you like. However, it's pretty easy to get rolling and really get up to speed with Firebase.

You've Released An App!

Your going to be famous! You've got a great app that simply lets users write to their very own "users" table. You've allowed them to save their first and last names in API Version 1.0:

Your API Needs To Be Improved

Now, 2 days after releasing your app, you realize that "Duh! That's not a very useful user's table!" So, you need to add email and phone as fields that users will be able to write, but you're stuck now. The old users ( you did get about 1M users in those 2 days, right?) can only write givenName and surname until the App Store Review Gods approve your new release 16 days later. You can't change the "API" yet because it would start failing for those users. Then, when the new release is approved, everyone using the new app will be trying to write the additional fields, email and phone, which will also fail. How can you have two sets of rules for this?

I've pondered this question before for my own app. Someone suggested having the new apps write to a different node with different rules and also write to the original node with the old rules. This would allow both old and new users to see the data. You'd do this until all your users upgraded and then could delete the old node and rules.

There are some problems with this method though. It means duplicate writes and duplicate data - more overhead and more work on the client. Later, it means cleaning up the mess. Also, how would you even know that all your clients had upgraded? It seemed like too much work for me. So, I just sort of "relaxed" my validation rules a bit to allow the different clients to write to the same node. However, this "relaxing" introduced some security issues.

Bad Solution

Welcome to "relaxed" rules (don't try this at home). So, you decide to relax the rules a bit and remove the $other validation. Your rules now look like this:

Notice that the $other validation was deleted? So, now users with either app can write to their "users" table. Tada!

Oh.... all of a sudden, your Firebase backups start going from 128MB to 256MB and then 1GB. Woohoo! You've hit the jackpot. Your app is a viral success! Or not....

In reality, your user base hasn't really grown all that much. You've just got a malicious user writing lots of data into their "users" table. Because the "$other" has been removed, they can write any additional fields they like. How about a porn array with 1000 base64 encoded photo strings? Good luck explaining that to the FBI.

Example Bad Data:

{
  "givenName": "bad",
  "surname": "guy",
  "porn": [
    {
      "omg": "whoa!"
    }
  ]
}

Validation Results:

Attempt to write Success({"givenName":"bad","porn":{"0":{"omg":"whoa!"}},"surname":"guy"}) to /usersTest/xkxkxk with auth=Success({"id":42,"provider":"anonymous","uid":"anonymous:42"})  
    /:.write: "false"
        => false
    /usersTest:.write: "true"
        => true
    /usersTest/xkxkxk:.validate: "newData.hasChildren(['givenName', 'surname'])"
        => true
    /usersTest/xkxkxk:.validate: "newData.hasChildren(['givenName', 'surname'])"
        => true
    /usersTest/xkxkxk/givenName:.validate: "newData.isString() && newData.val().length >= 1 && newData.val().length < 100"
        => true
    /usersTest/xkxkxk/surname:.validate: "newData.isString() && newData.val().length >= 1 && newData.val().length < 100"
        => true

Write was allowed.  

That porn property is completely ignored in the validation process. You've just opened up your "API" to anyone that wants to mess with you.

Good Solution

So, what's a clever developer like you to do? You don't want duplicate data all over the place and you care about security. I recently came up with this little trick to solve the problem.

First and foremost, KEEP THE $other VALIDATION RULE. Use the power of the Firebase rules to help you keep the API secure. You can use an OR statement in the node's ".validate" section to allow different fields from different clients.

Now, any attempts to write anything other than "givenName,surname" OR "givenName, surnam, email, phone" will fail the validation checks.

Example Bad Data:

{
  "givenName": "bad",
  "surname": "guy",
  "email" : "[email protected]",
  "phone" : "+15554443333",
  "porn": [
    {
      "omg": "whoa!"
    }
  ]
}

Validation Results:

Attempt to write Success({"email":"[email protected]","givenName":"bad","phone":"+15552223333","porn":{"0":{"omg":"whoa!"}},"surname":"guy"}) to /usersTest/xkxkxk with auth=Success({"id":42,"provider":"anonymous","uid":"anonymous:42"})  
    /:.write: "false"
        => false
    /usersTest:.write: "true"
        => true
    /usersTest/xkxkxk:.validate: "newData.hasChildren(['givenName', 'surname']) || newData.hasChildren(['givenName', 'surname', 'email', 'phone'])"
        => true
    /usersTest/xkxkxk:.validate: "newData.hasChildren(['givenName', 'surname']) || newData.hasChildren(['givenName', 'surname', 'email', 'phone'])"
        => true
    /usersTest/xkxkxk/email:.validate: "newData.isString() && newData.val().length >=5 && newData.val().length < 100"
        => true
    /usersTest/xkxkxk/givenName:.validate: "newData.isString() && newData.val().length >= 1 && newData.val().length < 100"
        => true
    /usersTest/xkxkxk/phone:.validate: "newData.isString() && newData.val().length >= 10 && newData.val().length < 16"
        => true
    /usersTest/xkxkxk/porn:.validate: "false"
        => false

Validation failed.  
Write was denied.  

I hope this trick proves useful to other Firebase users. If you've got some great ideas or improvements for this one, please let me know on Twitter.