In this lab, you will gain experience with Firebase, a backend-as-a-service solution that allows developers to rapidly prototype applications without having to implement common pieces of functionality. While platform offerings like Firebase can save time, they do lock the application into a specific vendor.

One of the key features of Firebase is its real-time database support. With modern web and mobile applications shifting more of its functionality to the client, backend servers that used to provide Model, View, and Controller functions now only implement a Model that is accessible via asynchronous API calls from the client. This lab will demonstrate this pattern using a chat application.

For this lab, we require a new project to be created. The project will be automatically deleted when we delete the associated Firebase project. Visit the web console and in the project selection window, click on "New Project". Name the project firebase-<OdinID> and associate it with your course billing account.

Firebase has a separate console on Google Cloud. Visit its console at https://console.firebase.google.com/ and add a project. When entering your project name, add it to the Google Cloud Platform project created above and click "Continue".

Confirm the defaults for the following prompts, but when asked to collect Google Analytics, turn them off.

Most web applications require a backend that supports clients running on mobile platforms (Android, iOS) as well as the web. While Firebase has support for all of them, we'll only be focusing on a web client version. From the Firebase console, visit the Project Overview and click on </> to register a new web application for the project.

Register the application.

Skip the next steps for including Firebase in your application. We will be using code from a repository and adding the necessary support to it in Cloud Shell.

From the Firebase console, visit Develop=>Authentication=>Sign-In Method and enable Google account logins for your web application.

Name the application FriendlyChat, set your e-mail address, and save the settings.

Firebase restricts access to its resources to specific domains for security purposes. We'll be testing our application from Cloud Shell. To enable access from it, we need to add it in the list of authorized domains.

Answer the following questions for your lab notebook

From the Firebase console, visit Develop=>Cloud Firestore and "Create database". Cloud Firestore is Firebase's latest real-time database and is a replacement for its older realtime database.

Enable "Start in test mode..."

From the Firebase console, visit Develop=>Storage=>Get Started.

The bucket's security rules default to public access. Keep this default setting by clicking "Next". Then, keep the storage region to the default setting.

Bring up the firebase-<OdinID> project on the Google Cloud Platform console. Then, bring up a Cloud Shell session and clone the repository and use Node's package manager (npm) to install the Firebase command-line interface.

git clone https://github.com/firebase/codelab-friendlychat-web

npm -g install firebase-tools

Verify that the CLI has been installed correctly by obtaining the version of Firebase.

firebase --version

Authorize your installation so that it can deploy resources on your project. We need to first logout in order to force a reauthorization and obtain an OAuth token configured with appropriate access:

firebase logout

firebase login --no-localhost

Visit the URL given and login to your account. Note that you will need to cut-and-paste the entire URL given in the console. Allow access for the Firebase CLI.

Then get the authorization code:

Paste it in to complete the login.

Change into the web-start directory, then connect the Firebase CLI to the Firebase project created earlier.

cd codelab-friendlychat-web/web-start

firebase use --add

Use the arrow keys to select your project and hit 'Enter'. Then, set the alias to 'default'.

In Cloud Shell, launch the code editor to view index.html

edit public/index.html

The HTML builds a basic skeleton for a UI. The elements within the skeleton will be populated via our application code in scripts/main.js. Scroll to the bottom of the file to see its inclusion. Also included at the bottom are the Javascript files that implement the Firebase SDK and its functionality. Each part of Firebase has its own file.

Answer the following for your lab notebook:

Finally, firebase/init.js is included. This file is created by the previous firebase use command and contains the project's Firebase credentials.

We will now view the application. Go back to the Cloud Shell terminal and ensure you are in the web-start directory. Launch the Firebase hosting emulator to serve the code. This emulator runs the application locally for testing purposes.

firebase serve --only hosting

The command will return a link to a local server.

Click the link to visit it or go to Web Preview, change the port to 5000 and then preview the page.

You should get the following application that isn't very functional (yet). Notice that the URL contains the domain that we enabled earlier in the Firebase project settings.

Perform a "View source" on the page. Scroll down to the bottom and click on the link to the init.js script that has been set-up via the firebase use command.

Answer the following questions:

Go back to Cloud Shell and type Ctrl+c to exit.

In Cloud Shell, launch the code editor to view the application's code at scripts/main.js. The file contains the functionality for implementing a chat application in which users can authenticate and send each other messages in real-time. The code is not completely filled in so that we can demonstrate how Firebase provides functionality to applications.

View the code and answer the following questions for your lab notebook:

We will be filling these functions in.

First, use the code below to implement signIn for when the user clicks the "Sign in with Google" button. With just two lines, we can add authentication using Google as the identity provider to the application (via OAuth). Note that we use a pop-up window to do so. Your browser must be enabled to allow pop-ups for this to work.

  // Sign into Firebase using popup auth & Google as the identity provider.
  var provider = new firebase.auth.GoogleAuthProvider();
  firebase.auth().signInWithPopup(provider);

Next, use the code below to implement signOut for when the user clicks "Sign out" in the UI.

  // Sign out of Firebase.
  firebase.auth().signOut();

We need to update the UI when a user changes his/her authentication state. The callback function is registered when we set authentication up in initFirebaseAuth. Add the following code to implement this function.

  // Listen to auth state changes.
  firebase.auth().onAuthStateChanged(authStateObserver);

The function that is registered is authStateObserver(). Scroll down to the function and see that it hides and unhides the UI elements of the HTML page based on whether or not the user is signed in. At the bottom of this function, view the code for when a user is not signed in.

Answer the following questions:

Above this code, is code that sets the UI elements for when the user is logged in. It retrieves both the profile picture URL and the name of the user from the user's authentication information provided by Google. We will fill in the code that implements them.

First, implement getProfilePicUrl with the code below that pulls the URL from the user's authentication context:

  return firebase.auth().currentUser.photoURL || '/images/profile_placeholder.png';

Then, implement getUserName that pulls the display name:

  return firebase.auth().currentUser.displayName;

Finally, if a user attempts to send a message while not logged in, the application generates an error message. Implement the isUserSignedIn function to perform this check.

  return !!firebase.auth().currentUser;

Save the file after making the changes.

Change into the web-start directory and launch the Firebase hosting emulator to serve the Firebase application again from the Cloud Shell command-line. Visit the site and attempt to send a message. If the code for isUserSignedIn is working properly, an error message should appear.

Then, click on "SIGN-IN WITH GOOGLE". Login to your PSU account and see that its profile picture and name have now been included in the UI.

Some troubleshooting notes:

Attempt to send another message. While the error message no longer appears, because we haven't added the code for messaging, nothing happens. Return to Cloud Shell and exit out of the emulator.

We will now implement the messaging functionality. In Cloud Shell, go back to the code editor and scripts/main.js. Find the stub for sending text messages when the "Send" button is clicked. We'll be using Firestore to send messages. The code uses the Firestore API's add() call to add a message to the appropriate collection. A JSON object containing the user's name, the text, the URL of the user's profile picture, and the timestamp are added to the database.

  // Add a new message entry to the database. 
  return firebase.firestore().collection('messages').add({
           name: getUserName(),
           text: messageText,
           profilePicUrl: getProfilePicUrl(),
           timestamp: firebase.firestore.FieldValue.serverTimestamp()
  }).catch(function(error) {
    console.error('Error writing new message to database', error);
  });

To view the messages that have been sent, we must also update the code for rendering messages in the UI. Visit the stub that implements the loading of messages from the Firestore backend and paste in the code below. The code creates a Firestore query on the messages collection asking for the most recent 12 messages in the collection. The power of the real-time database functionality that Firestore provides comes in the next statement where the query creates a listener that triggers when the results of the query change. It does so by registering a callback via its onSnapshot method. The callback code removes the DOM elements of deleted messages and displays any new messages that have been sent.

  // Create the query to load the last 12 messages and listen for new ones.
  var query = firebase.firestore()
                  .collection('messages')
                  .orderBy('timestamp', 'desc')
                  .limit(12);
  
  // Start listening to the query.
  query.onSnapshot(function(snapshot) {
    snapshot.docChanges().forEach(function(change) {
      if (change.type === 'removed') {
        deleteMessage(change.doc.id);
      } else {
        var message = change.doc.data();
        displayMessage(change.doc.id, message.timestamp, message.name,
                       message.text, message.profilePicUrl, message.imageUrl);
      }
    });
  });

All clients that visit the application share access to the Firestore database and can now synchronize messages in real-time. As Firebase supports mobile platforms, native clients on mobile platforms such as Android and iOS can also bring up the messages. Codelabs for doing so for this application can be found here and here.

Go back to Cloud Shell and serve your application again. Sign-in, then send a message. The message will be inserted into the Firestore database and the UI will automatically be updated with both the message and the account profile picture.

Go back to the Firebase console and bring up the application's Firestore database. Expand the messages collection and find the document containing the message within it.

Within the Cloud Firestore UI, we will now manually add a message and see that it updates the UI in real-time automatically. Under the messages collection, click "Add document", then click on "Auto-ID" to generate a unique Document ID. Then, create fields with the associated data types using the UI that have the following values:

Save the document and re-visit the Friendly Chat application.

As a database, Cloud Firestore is good for storing structured data. For unstructured files such as image uploads, Cloud Storage is more appropriate. The next step will allow users to upload images to the chat application which will then be stored in Cloud Storage. A URL link to the image will then be included in the messages' data. When the application comes across an image URL, it simply returns the contents of it to the application.

In Cloud Shell, bring up the code editor and find the stub for handling images. Use the following code to implement it:

  firebase.firestore().collection('messages').add({
    name: getUserName(),
    imageUrl: LOADING_IMAGE_URL,
    profilePicUrl: getProfilePicUrl(),
    timestamp: firebase.firestore.FieldValue.serverTimestamp()
  }).then(function(messageRef) {
    var filePath = firebase.auth().currentUser.uid + '/' + messageRef.id + '/' + file.name;
    return firebase.storage().ref(filePath).put(file).then(function(fileSnapshot) {
      return fileSnapshot.ref.getDownloadURL().then((url) => {
        return messageRef.update({
          imageUrl: url,
          storageUri: fileSnapshot.metadata.fullPath
        });
      });
    });
  }).catch(function(error) {
    console.error('There was an error uploading a file to Cloud Storage:', error);
  });

Through a series of Javascript promises, the code

In examining the code, answer the following question for you lab notebook:

Go back to Cloud Shell and serve your application again. Sign-in, then click on the image icon and send an image.

Go back to the Firebase console and bring up the application's Firestore database. Expand the messages collection and find the document containing the message within it.

Answer the following questions:

Visit the "Storage" section in the Firebase console and take a screenshot of the image in the storage bucket for your lab notebook.

Up until now, we have been using the Firebase emulator within Cloud Shell to host the site. When the application is ready, we can deploy it as a managed application in Firebase using Firebase's hosting service. The service automatically distributes the static assets across multiple data centers and replicates the content based on usage. This is similar to other serverless platform solutions such as App Engine.

To manage the deployment, Firebase uses a JSON file. Bring up the file in firebase.json.

Answer the following questions:

In the directory containing the deployment file, deploy the application:

firebase deploy --except functions

After deployment has finished, the URL for the deployment is returned. Note that its domain (web.app) has been previously authorized by our Firebase application during the project's setup. Visit the site and send the URL to someone you know in the class or the course instructor to add a message to your application.

Take a screenshot of the message including the URL for your lab notebook.

Go back to the Firebase console and visit the project's settings.

Scroll to the bottom and "Delete Project".

Confirm all selections to delete the project both from the Firebase and Google Cloud Platform consoles.