Firebase Cloud Functions

Firebase Functions are a way to run server-side code hosted by Firebase servers. It is an easy way to implement routes, or code that reacts to certain events, like changes to a database. The code is written in Javascript or TypeScript.

Hello World

From the Firebase Console, go to Functions. From there they guide you on how to set it up. You have to install some CLI tool, then run it to sign in, and create a skeleton of your function. One great thing, they support TypeScript on top of Javascript, and also TSLint to catch common errors.

You can deploy the skeleton right away. This skeleton defines one route that simply returns "Hello from Firebase".

Reacting to a change in Realtime Database

The Get started tutorial is well written. It defines a route that writes a new entry to the /messages path in Firebase Realtime Database. Then it has a listener to the /messages path, to read and output the uppercased version of what has just been written. The example is written in Javascript but it is painless to port to TypeScript. Here's how the port looks like:

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';

admin.initializeApp();

export const addMessage = functions.https.onRequest(async (req, res) => {
  // [END addMessageTrigger]
    // Grab the text parameter.
    const original = req.query.text;
    // [START adminSdkPush]
    // Push the new message into the Realtime Database using the Firebase Admin SDK.
    const snapshot = await admin.database().ref('/messages').push({original: original});
    // Redirect with 303 SEE OTHER to the URL of the pushed object in the Firebase console.
    res.redirect(303, snapshot.ref.toString());
    // [END adminSdkPush]
  });

export const makeUppercase = functions.database.ref('/messages/{pushId}/original')
    .onCreate((snapshot, context) => {
      // Grab the current value of what was written to the Realtime Database.
      const original = snapshot.val();
      console.log('Uppercasing', context.params.pushId, original);
      const uppercase = original.toUpperCase();
      // You must return a Promise when performing asynchronous tasks inside a Functions such as
      // writing to the Firebase Realtime Database.
      // Setting an "uppercase" sibling in the Realtime Database returns a Promise.
      if (snapshot.ref == null || snapshot.ref.parent == null) return;

      return snapshot.ref.parent.child('uppercase').set(uppercase);
    });

The Firebase tooling is great by the way. After running firebase deploy, it will print out the routes that were created. Also in the Firebase Console, you can check usage and logs of your functions in real time. Whatever is console.logged is printed out as Information level log. If your code crashed it'll automatically be logged as an Error level log.

Also since it's written in TypeScript, autocompletion in Visual Studio Code is perfect.

Final version with Firestore

The caveat in the code above is that it listens to Realtime Database, not Firebase's newer Firestore!! To support Firestore, the code becomes:

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';

export const notifyOnNewMessage = functions.firestore.document('/messages/{messageId}')
  .onCreate((snapshot, context) => {
    if (snapshot == null) return;

    console.log(context.params.messageId);
    console.log(snapshot.data);

    const notificationContent = {
      notification: {
        title: 'New message',
        body: snapshot.get('content'),
        icon: 'default'
      }
    };
    return admin.messaging().sendToTopic('all', notificationContent).then(result => {
      console.log('Notification sent!');
    });
  });
});

To read more, check out the documentation at https://firebase.google.com/docs/firestore/extend-with-functions.