Build Presence in Cloud Firestore

Depending on the type of app you're building, you might find it useful to detect which of your users or devices are actively online — otherwise known as detecting "presence."

For example, if you're building an app like a social network or deploying a fleet of IoT devices, you could use this information to display a list of friends that are online and free to chat, or sort your IoT devices by "last seen."

Cloud Firestore doesn't natively support presence, but you can leverage other Firebase products to build a presence system.

Solution: Cloud Functions with Realtime Database

To connect Cloud Firestore to Firebase Realtime Database's native presence feature, use Cloud Functions.

Use Realtime Database to report connection status, then use Cloud Functions to mirror that data into Cloud Firestore.

Using presence in Realtime Database

First, consider how a traditional presence system works in Realtime Database.

Web

// Fetch the current user's ID from Firebase Authentication.
const uid = firebase.auth().currentUser.uid;

// Create a reference to this user's specific status node.
// This is where we will store data about being online/offline.
const userStatusDatabaseRef = firebase.database().ref(`/status/${uid}`);

// We'll create two constants which we will write to 
// the Realtime database when this device is offline
// or online.
const isOfflineForDatabase = {
    state: "offline",
    last_changed: firebase.database.ServerValue.TIMESTAMP,
};

const isOnlineForDatabase = {
    state: "online",
    last_changed: firebase.database.ServerValue.TIMESTAMP,
};

// Create a reference to the special ".info/connected" path in 
// Realtime Database. This path returns `true` when connected
// and `false` when disconnected.
firebase.database().ref(".info/connected").on("value", function (snapshot) {
    // If we're not currently connected, don't do anything.
    if (snapshot.val() == false) {
        return;
    };

    // If we are currently connected, then use the 'onDisconnect()' 
    // method to add a set which will only trigger once this 
    // client has disconnected by closing the app, 
    // losing internet, or any other means.
    userStatusDatabaseRef.onDisconnect().set(isOfflineForDatabase).then(function () {
        // The promise returned from .onDisconnect().set() will
        // resolve as soon as the server acknowledges the onDisconnect() 
        // request, NOT once we've actually disconnected:
        // https://firebase.google.com/docs/reference/js/firebase.database.OnDisconnect

        // We can now safely set ourselves as "online" knowing that the
        // server will mark us as offline once we lose connection.
        userStatusDatabaseRef.set(isOnlineForDatabase);
    });
});

This example is a complete Realtime Database presence system. It handles multiple disconnections, crashes and so on.

Connecting to Cloud Firestore

To implement a similar solution in Cloud Firestore use the same Realtime Database code, then use Cloud Functions to keep Realtime Database and Cloud Firestore in sync.

If you haven't already, add Realtime Database to your project and include the above presence solution.

Next you'll synchronize the presence state to Cloud Firestore through the following methods:

  1. Locally, to the offline device's Cloud Firestore cache so that the app knows it's offline.
  2. Globally, using a Cloud Function so that all other devices accessing Cloud Firestore know this specfic device is offline.

Updating Cloud Firestore's local cache

Let's take a look at the changes required to fulfill the first issue - updating Cloud Firestore's local cache.

Web

// ...
const userStatusFirestoreRef = firebase.firestore().doc(`/status/${uid}`);

// Firestore uses a different server timestamp value, so we'll 
// create two more constants for Firestore state.
const isOfflineForFirestore = {
    state: "offline",
    last_changed: firebase.firestore.FieldValue.serverTimestamp(),
};

const isOnlineForFirestore = {
    state: "online",
    last_changed: firebase.firestore.FieldValue.serverTimestamp(),
};

firebase.database().ref(".info/connected").on("value", function (snapshot) {
    if (snapshot.val() == false) {
        // Instead of simply returning, we'll also set Firestore's state
        // to "offline". This ensures that our Firestore cache is aware
        // of the switch to "offline."
        userStatusFirestoreRef.set(isOfflineForFirestore);
        return;
    };

    userStatusDatabaseRef.onDisconnect().set(isOfflineForDatabase).then(function () {
        userStatusDatabaseRef.set(isOnlineForDatabase);

        // We'll also add Firestore set here for when we come online.
        userStatusFirestoreRef.set(isOnlineForFirestore);
    });
});

With these changes we've now ensured that the local Cloud Firestore state will always reflect the online/offline status of the device. This means you can listen to the /status/{uid} document and use the data to change your UI to reflect connection status.

Web

userStatusFirestoreRef.onSnapshot(function (doc) {
    const isOnline = doc.data().state == "online";
    // ... use isOnline
});

Updating Cloud Firestore globally

Although our application correctly reports online presence to itself, this status will not be accurate in other Cloud Firestore apps yet because our "offline" status write is local only and won't be synced up when a connection is restored. To counter this, we'll use a Cloud Function which watches the status/{uid} path in Realtime Database. When the Realtime Database value changes the value will sync to Cloud Firestore so that all users' statuses are correct.

Node.js

const functions = require('firebase-functions');
const Firestore = require('@google-cloud/firestore');

// Since this code will be running in the Cloud Functions enviornment
// we call initialize Firestore without any arguments because it
// detects authentication from the environment.
const firestore = new Firestore();

// Create a new function which is triggered on changes to /status/{uid}
// Note: This is a Realtime Database trigger, *not* Cloud Firestore.
exports.onUserStatusChanged = functions.database
    .ref("/status/{uid}").onUpdate((event) => {
        // Get the data written to Realtime Database
        const eventStatus = event.data.val();

        // Then use other event data to create a reference to the
        // corresponding Firestore document.
        const userStatusFirestoreRef = firestore.doc(`status/${event.params.uid}`);

        // It is likely that the Realtime Database change that triggered
        // this event has already been overwritten by a fast change in
        // online / offline status, so we'll re-read the current data
        // and compare the timestamps.
        return event.data.ref.once("value").then((statusSnapshot) => {
            return statusSnapshot.val();
        }).then((status) => {
            console.log(status, eventStatus);
            // If the current timestamp for this data is newer than
            // the data that triggered this event, we exit this function.
            if (status.last_changed > eventStatus.last_changed) return;

            // Otherwise, we convert the last_changed field to a Date
            eventStatus.last_changed = new Date(eventStatus.last_changed);

            // ... and write it to Firestore.
            return userStatusFirestoreRef.set(eventStatus);
        });
    });

Once you deploy this function, you'll have a complete presence system running with Cloud Firestore. Below is an example of monitoring for any users who come online or go offline using a where() query.

Web

firebase.firestore().collection("status")
    .where("state", "==", "online")
    .onSnapshot(function (snapshot) {
        snapshot.docChanges.forEach(function(change) {
            if (change.type === "added") {
                console.log(`User ${change.doc.id} is now online.`);
            }
            if (change.type === "removed") {
                console.log(`User ${change.doc.id} has gone offline.`);
            }
        });
    });

Limitations

The solution shown above is a scalable way to create a presence system in Cloud Firestore, but be aware that it's likely to trigger multiple changes when listening to realtime changes in Cloud Firestore. If the changes trigger more events than you want, manually debounce the Cloud Firestore events.

This implementation measures connectivity to Realtime Database, not Cloud Firestore so it is possible that it may not be correct at all times.

Send feedback about...

Need help? Visit our support page.