Extend Realtime Database with Cloud Functions


With Cloud Functions, you can handle events in the Firebase Realtime Database with no need to update client code. Cloud Functions lets you run Realtime Database operations with full administrative privileges, and ensures that each change to Realtime Database is processed individually. You can make Firebase Realtime Database changes via the DataSnapshot or via the Admin SDK.

In a typical lifecycle, a Firebase Realtime Database function does the following:

  1. Waits for changes to a particular Realtime Database location.
  2. Triggers when an event occurs and performs its tasks (see What can I do with Cloud Functions? for examples of use cases).
  3. Receives a data object that contains a snapshot of the data stored in the specified document.

Trigger a Realtime Database function

Create new functions for Realtime Database events with functions.database. To control when the function triggers, specify one of the event handlers, and specify the Realtime Database path where it will listen for events.

Set the event handler

Functions let you handle Realtime Database events at two levels of specificity; you can listen for specifically for only creation, update, or deletion events, or you can listen for any change of any kind to a path. Cloud Functions supports these event handlers for Realtime Database:

  • onWrite(), which triggers when data is created, updated, or deleted in Realtime Database.
  • onCreate(), which triggers when new data is created in Realtime Database.
  • onUpdate(), which triggers when data is updated in Realtime Database .
  • onDelete(), which triggers when data is deleted from Realtime Database .

Specify the instance and path

To control when and where your function should trigger, call ref(path) to specify a path, and optionally specify a Realtime Database instance with instance('INSTANCE_NAME'). If you do not specify an instance, the function deploys to the default Realtime Database instance for the Firebase project For example:

  • Default Realtime Database instance: functions.database.ref('/foo/bar')
  • Instance named "my-app-db-2": functions.database.instance('my-app-db-2').ref('/foo/bar')

These methods direct your function to handle writes at a certain path within the Realtime Database instance. Path specifications match all writes that touch a path, including writes that happen anywhere below it. If you set the path for your function as /foo/bar, it matches events at both of these locations:

 /foo/bar
 /foo/bar/baz/really/deep/path

In either case, Firebase interprets that the event occurs at /foo/bar, and the event data includes the old and new data at /foo/bar. If the event data might be large, consider using multiple functions at deeper paths instead of a single function near the root of your database. For the best performance, only request data at the deepest level possible.

You can specify a path component as a wildcard by surrounding it with curly brackets; ref('foo/{bar}') matches any child of /foo. The values of these wildcard path components are available within the EventContext.params object of your function. In this example, the value is available as context.params.bar.

Paths with wildcards can match multiple events from a single write. An insert of

{
  "foo": {
    "hello": "world",
    "firebase": "functions"
  }
}

matches the path "/foo/{bar}" twice: once with "hello": "world" and again with "firebase": "functions".

Handle event data

When handling a Realtime Database event, the data object returned is a DataSnapshot. For onWrite or onUpdate events, the first parameter is a Change object that contains two snapshots that represent the data state before and after the triggering event. For onCreate and onDelete events, the data object returned is a snapshot of the data created or deleted.

In this example, the function retrieves the snapshot for the specified path, converts the string at that location to uppercase, and writes that modified string to the database:

// Listens for new messages added to /messages/:pushId/original and creates an
// uppercase version of the message to /messages/:pushId/uppercase
exports.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();
      functions.logger.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.
      return snapshot.ref.parent.child('uppercase').set(uppercase);
    });

Accessing user authentication information

From EventContext.auth and EventContext.authType, you can access the user information, including permissions, for the user that triggered a function. This can be useful for enforcing security rules, allowing your function to complete different operations based on the user's level of permissions:

const functions = require('firebase-functions');
const admin = require('firebase-admin');

exports.simpleDbFunction = functions.database.ref('/path')
    .onCreate((snap, context) => {
      if (context.authType === 'ADMIN') {
        // do something
      } else if (context.authType === 'USER') {
        console.log(snap.val(), 'written by', context.auth.uid);
      }
    });

Also, you can leverage user authentication information to "impersonate" a user and perform write operations on the user's behalf. Make sure to delete the app instance as shown below in order to prevent concurrency issues:

exports.impersonateMakeUpperCase = functions.database.ref('/messages/{pushId}/original')
    .onCreate((snap, context) => {
      const appOptions = JSON.parse(process.env.FIREBASE_CONFIG);
      appOptions.databaseAuthVariableOverride = context.auth;
      const app = admin.initializeApp(appOptions, 'app');
      const uppercase = snap.val().toUpperCase();
      const ref = snap.ref.parent.child('uppercase');

      const deleteApp = () => app.delete().catch(() => null);

      return app.database().ref(ref).set(uppercase).then(res => {
        // Deleting the app is necessary for preventing concurrency leaks
        return deleteApp().then(() => res);
      }).catch(err => {
        return deleteApp().then(() => Promise.reject(err));
      });
    });

Reading the previous value

The Change object has a before property that lets you inspect what was saved to Realtime Database before the event. The before property returns a DataSnapshot where all methods (for example, val() and exists()) refer to the previous value. You can read the new value again by either using the original DataSnapshot or reading the after property. This property on any Change is another DataSnapshot representing the state of the data after the event happened.

For example, the before property can be used to make sure the function only uppercases text when it is first created:

exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
    .onWrite((change, context) => {
      // Only edit data when it is first created.
      if (change.before.exists()) {
        return null;
      }
      // Exit when the data is deleted.
      if (!change.after.exists()) {
        return null;
      }
      // Grab the current value of what was written to the Realtime Database.
      const original = change.after.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.
      return change.after.ref.parent.child('uppercase').set(uppercase);
    });