Our extensive experience with Cloud Functions for Firebase
We at Step Up Labs have been working with Google for the past year or so and are very excited to see the product finally make it to the public beta. As I’ve been working with the Cloud Functions for quite some time now, I thought I’d share some of the details of how we use them in our main product — Settle Up — and the experience we have gained so far. I won’t be describing the API or going over how exactly to work with the Functions, you have the official docs and samples for that.
About Settle Up
Let me first briefly tell you what the app does. Settle Up is an app for everybody who needs to track shared expenses. It’s perfect for travelers, flatmates and friends who don’t want to argue over who should pay next or who owes to whom.
In the app, you can create separate groups (e.g. “Skiing trip 2017”). Within each group you can enter members (your friends who went skiing with you) and individual expenses (gas, tickets, hotel, etc.). Based on these expenses, the app can tell you at the end of the trip who should pay whom to break even. One useful feature is that the individual expenses can even be in different currencies, while the debts are displayed in your “home” currency.
All this information — groups, members, expenses and other data — is managed by individual client applications and stored in Firebase Database. There are however some vital parts which we decided to delegate to Cloud Functions.
Reacting to group changes
One thing we want the users of Settle Up to see are the changes done within each group. For example when someone deletes an expense there should be a record about that showing who deleted it. This is a perfect use case for the Cloud Functions. The snippet below roughly shows how we do it (simplified for clarity):
Notice a couple of things here.
- We use async / await which simplifies the code by removing all those annoying callback functions and makes it look like any other synchronous code. This is currently a Typescript-only feature which compiles down to generators. The good thing is it uses Promises which the Cloud Functions work with out of the box.
- We use admin.database.ServerValue.TIMESTAMP placeholder for setting server time.
- We need to find out who was the author of the change. There is currently no documented way to do this, only undocumented one — the user id is set inside event.auth.variable.uid. There are situations when this isn’t set (making changes directly in Firebase Console for example) so you need to have a check for this, otherwise the by variable becomes undefined, which cannot be saved into Firebase Database.
Sending push notifications
Another step after generating a change in the app is to send a push notification to subscribed clients. When a user logs in on their device the app requests a registration token and saves it to Firebase Database. This is what our structure looks like:
There is a nice official sample how to send push notifications with Firebase on github so you can start there. I’ll just outline a few things:
- use the Firebase Admin SDK to send notifications, so you won’t have to deal with authenticating to Firebase servers and doing manual HTTP requests;
- you cannot send null values in your payload;
- be sure to process the results of the send..() methods and remove the tokens which FCM servers mark as invalid (as seen in the github sample);
- tailor the notifications to the individual platforms. Notice we have the platform information for each push registration which allows us to create a different payload for each platform. The payload structure is described here.
This snippet shows two types of payload — the raw one, which is sent to the web app and Android devices, and the iOS one sent to iPhone users. The raw payload contains plain JSON which the Android version of Settle Up processes on the background and groups corresponding notifications together into single notification. It works similarly with our web app.
The iOS payload on the other hand doesn’t allow any processing and is simply presented on the device as it arrives. Using body_loc and title_loc properties ensures that the notifications are correctly localized.
Working with exchange rates
As Settle Up allows to enter expenses in various currencies we needed a way to work with exchange rates to allow conversion into one group currency. For that there are three pieces of puzzle required:
- store exchange rate between expense currency and group currency for each expense;
- have a centralized storage of exchange rates within Firebase Database;
- allow changing group currency to show debts in a different currency.
Having a single location for exchange rates within our database simplifies development of the client apps — to get a currency rate they only need to access single Firebase location and don’t need to worry about anything else. This location has exchange rates between any world currency and USD, gets updated daily and keeps the history for every day. There is also a special node called “latest” which has the latest exchange rates.
To fetch the latest rates every day, we use an HTTPS trigger in Cloud Functions and Google App Engine CRON job which invokes it every day.
One thing I’d like to point out is the response.end() which properly closes the incoming HTTPS requests. Without it the incoming requests will time out.
As you can see, we use Yahoo as a source of exchange rates, but this is an implementation detail which we can easily change in our Cloud Functions without having to make any changes to the client apps.
Changing the group currency
Changing the group currency is not very straight-forward. Before actually changing it we need to iterate over all expenses in the given group and update their exchange rates. Only after all expenses have the updated exchange rates can we update the group currency. It’s obvious that this needs to be a server function.
We decided to have a dedicated location in Firebase Database called “serverTasks” and have a cloud function watching this location and triggering corresponding functions. Sure, it could be just an HTTPS request (like in the case of getting the latest exchange rates every day) but that would complicate the client code. Simply writing to a Firebase location and observing it to get the result of the operation was just easier.
Here’s what the Database structure might look like:
And here’s the function to change the group currency itself along with registration:
Again a few notes about the implementation:
- We use adminRef to write the response code because the Database Rules prevent anyone else from writing it. This is by design because we only want the Cloud Functions to be able to write there. It is generally a good idea to spend a bit of time thinking about your Rules and the implications they will have.
- The function writes the updates at the very end in a form of a single multi-location update. This is useful for two reasons: it ensures we don’t end up in a corrupt state in case of an error halfway through processing and also, it is faster.
- Before writing the data the function first checks whether the original task still exists. If not, it means that processing of the task took too long and the client app deleted it.
Shortening Dynamic links
To join a group the user must be invited via a Dynamic link. When the user clicks the link they are redirected to the Settle Up app and they are offered to join the group. The link is generated locally by the client app and looks something like this:
That’s a pretty long and ugly link. It is however possible to shorten it using a single HTTP POST. Again, to simplify the client code, this shortening is done on the server:
There are two points of interest here. One is how we check whether the link is long (not shortened) — simply by inspecting whether it contains “?link”. This is probably over-simplified, but it works.
The other one has to do with multiple environments. You see, we actively use three environments — sandbox, alpha, and live. The endpoint for shortening links requires an API key of our app (~environment) and since we have three of them we need to make sure the correct one is used in each one of them.
To achieve that we use Firebase Config. Setting up the config is currently a set of the following commands ran once against each environment:
firebase use sandbox
firebase functions:config:set local.apikey="<APIKEY>"
firebase deploy --only functions
As you can see, we use Firebase and Cloud Functions quite heavily. We also use them in some other scenarios I haven’t mentioned — for example deleting an entire group (which deletes data from multiple locations, as well as from Firebase Storage).
Overall, we’re pleased with where the development is going and the progress the Firebase team have made. Functions are extremely useful to us for a bunch of reasons:
- They heavily simplify the development of our client apps — what would need to be written multiple times for each client platform can only be written once.
- Eliminate various edge cases — some functionality could suffer from them if it were implemented in client apps, for example during loss of internet connectivity.
- Our client apps are more secure — we don’t need to store API keys to various services within the apps but only on the server.
- Sending push notifications is incredibly easy to implement.
In terms of performance and stability, this has fluctuated during the pre-beta phase of the product but has become pretty reliable as it was approaching the beta release. As far as we can tell, there are no critical issues now that would prevent you from using the Cloud Functions effectively.