Send notifications with Firebase from Firestore events using Cloud Functions

Firebase is great for quick MVPs when you don’t want to invest too much time on the server side. Let’s look at one of the most powerful Firebase features, Cloud Functions. With this, you can write any code that will be executed on Firebase’s servers to create custom web hooks or to respond to changes in the database.

Firestore Schema

Let’s take example of a chat app and see how we can send notifications to users on new incoming messages with these functions. It depends heavily on the database schema, so let’s quickly go over the schema we will be handling with Firestore.

Firestore DB Schema

So whenever a new message is sent, we create a new Thread and add it to the active channel.

Function

The first step is to set up your project for development. Firebase has a good guide on it.

We will use the onCreate function on firestore document to be notified whenever a new record is created in a collection.

exports.sendChatNotifications = functions.firestore.document('channels/{cid}/thread/{did}').onCreate(async (threadSnapshot, context) => {
  // ...
});

The next step is to find all the participants of the channel this message was sent to.

const participants = await admin.firestore().collection('channel_participation').where('channel', '==', context.params.cid).get();

After we find the participants, we will use sendToDevice method on the admin messaging API to actually send out the notification. Don’t forget the sound key to make sure the notification makes a sound on all devices. We can also include custom image and icon in the notification by passing urls to images. Here is the full code with everything put together.

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

admin.initializeApp({
  credential: admin.credential.applicationDefault(),
  databaseURL: '...'
});

exports.sendChatNotifications = functions.firestore.document('channels/{cid}/thread/{did}').onCreate(async (threadSnapshot, context) => {
  const thread = threadSnapshot.data();
  const senderId = thread.senderID;

  console.log(`Finding channel participants with channel ${context.params.cid}`);
  const participants = await admin.firestore().collection('channel_participation').where('channel', '==', context.params.cid).get();
  if (participants.empty) {
    console.log('There are no participants to send to.');
    return;
  }

  participants.forEach(async (snapshot) => {
    const uid = snapshot.data().user;
    if (uid === senderId) { return; }

    const user = await admin.firestore().doc(`users/${uid}`).get();

    const payload = {
      notification: {
        title: thread.senderFirstName,
        body: thread.content,
        icon: thread.senderProfilePictureURL,
        image: thread.url,
        sound: 'default',
      }
    };
    await admin.messaging().sendToDevice([user.data().fcmToken], payload);
  });
});

All we need now is to deploy this function. We can do that with yarn firebase deploy --only=functions

Published 20 Jun 2020

I build mobile and web applications. Full Stack, Rails, React, Typescript, Kotlin, Swift
Pulkit Goyal on Twitter