Skip links

Working with Firebase

During the Christmas break, I finally had a chance to work on Firebase SDK and test out some of its features. Firebase is a Backend as a Service that allows you to create mobile and web applications quickly. I wanted to test out the theory of whether it was possible to build something substantial using Firebase. The idea was to build an e-commerce web app using Firebase and integrate it with 3rd party services like Stripe and MailChimp. The idea was also to explore the concepts of rapid app development by using serverless technologies.

Being a frontend developer, I find that Firebase could be a suitable technology to master that can allow me to build end-to-end solutions and apps.

For the exercise, I chose to develop an e-commerce application as I believed that it provides the ingredients to use the maximum number of services that Firebase provides. With just a little over that 3 days of work, the app is far from finished, however, I would like to take the opportunity to share some of my experiences working with Firebase.

So for my app, I basically have two frontend implementations that I am building using ReactJS. The first one is the e-commerce app itself and the second one is the admin app where I will receive the information for all the orders received and also manage the products and fulfilment of the orders.

Setting up Firebase Hosting for multi-sites

One of the requirements I identified when developing the app was having two distinct sites for the admin and the customer-facing web apps. Firebase Hosting is great for this as it allows us to create up-to 36 distinct sites in a single project. What this allows us to do is that all these sites can share the same resources like Auth, Firestore, Realtime Database and Storage. This is great when you are creating apps that has various sub-apps like blog or in our case an admin app for managing the orders.

Setting up the multi-site hosting was pretty straight forward. My Bitbucket repo was setup with lerna and I had packages for admin and frontend for my two front-end apps and backend for my functions app.

Project Setup using lerna

In both of the apps, I had initialised a custom react app which was setup with the same firebase project. Deploying to firebase hosting is really easy. Basically you build your app using whatever framework you are comfortable with and then deploy the published folder to firebase hosting using the command:

$ firebase deploy --only hosting

Multi-sites implementation on Firebase Hosting

Now one of the issues I encountered was that I needed to deploy the admin site to the admin-ecommerce-8c3a8 site and the frontend to the ecommerce-8c3a8 site. Moreover, I wanted to use the same command firebase deploy --only hosting in both my apps.

For the frontend app the deployment was straightforward, I could use the same command as it was configured to deploy to the default hosting site. But for admin I needed to make the following changes. In the admin app I updated the target of my hosting to go to admin-ecommerce-8c3a8 site. This was done by running the following command in the root folder of the admin app:

$ firebase target:apply hosting admin admin-ecommerce-8c3a8

This command updated the .firebaserc file in my admin app to:

{
  "projects": {
    "default": "ecommerce-8c3a8"
  },
  "targets": {
    "ecommerce-8c3a8": {
      "hosting": {
        "admin": [
          "admin-ecommerce-8c3a8"
        ]
      }
    }
  }
}

The next thing I wanted to do was use the same deployment command as for the default site. To accomplish this, I updated the firebase.json file’s hosting property to be an array and added the target value as admin. This ensures that I can deploy the admin site to the admin hosting section on firebase with having to explicitly specify the target name on deployment.

Next up, with the two sites configured with two different react apps, I needed to implement firebase Auth to allow only authenticated users to access the admin app.

Firebase Auth (very easy to implement)

Implementing a user auth functionality is a big piece of work. Ask any seasoned developer and most of the favourite stories to share around drinks is how hard implementing a good user authentication system is. Firebase really makes it easy for developers to implement authentication in your apps. It is literally a few lines of plug and play code and everything works seamlessly.

However, first, you need to set-up a sign-in method in the Firebase project on the web console.

Initial screen for implementing Firebase Auth

In Firebase, you have a selection of sign-in methods, from email/password to a number of federated single sign-ons like facebook, Google, Microsoft and even Apple.

Options for implementing various federated authentication in Firebase

All that is required is to click on the pencil icon on the right hand side and enable the authentication flow. For my example, I was only going to use the Email/Password option.

To implement the Email/Password sign-in method, I created a Login component and within the function that handles the submission of the credentials, I make the function call as below:

const lgn = fire.auth().signInWithEmailAndPassword(email.value, password.value)

This provides me with a JWT that allows me to authenticate as a user. I can use the JWT token to access the protected sections of my admin app.

Firestore and Data Modelling

Firebase’s crown jewel is its NoSQL realtime database – Firestore. Firestore is different from relational database as it is schema-less. This provides a lot of flexibility on how to store your data as well as provides some challenges in how to store this data optimally. Being schema-less and NoSQL based, you don’t have big querying capabilities in Firestore, however the database is modelled on key-value storage which makes it extremely fast to query and traverse through the results. In Firestore, you store data as a combination of collections and documents. Collections are logical groupings of data like Customers, Products, Orders etc whereas each record in a collection is called a document.

Since Firestore is a NoSQL database, implementing complex queries and joins across different collections or different Firestore instances is not possible. However, Firestore does provide a few querying capabilities like using the WHERE clause to filter through results.

For my app, I plan to implement the following collections: Products, Subscribers, Customers and Orders. There is going to be a dependency between Customers and Orders which I haven’t had a chance to implement in my app yet.

However, getting started with Firestore was just as easy as Auth. In order for me to get the list of products to show on my home-page, I implemented the following code in the useEffect() hook within my App page component:

useEffect(() => {
    let items;
    const dbRef = fire.firestore();
    dbRef.collection("products")
      .get()
      .then(collection => {
        console.log(collection.docs);
        items = collection.docs.map(product => {
          const data = product.data();
          const id = product.id;
          return {id, ...data};
        })
        setProducts(items);
        isLoading(false);
      })
      .catch(err => {
        console.log("There seems to be an error fetching the products:", err);
      })
  },[]);

Here, I am using the firebase instance to connect to firestore. I am then able to get a list of products from the products collection and set them to the local state of the app.

The best thing about Firestore is its realtime capabilities. This allows us to make a change in the database in one place and the change gets propagated throughout the app. This realtime capability is extremely difficult to implement and Firestore have implemented this from the initial version of the Realtime Database.

Cloud Storage

To store unstructured data like images and assets, Firebase gives us the option of Cloud Storage to store such items. I used Cloud Storage to store the images of the products. In the admin app, when creating a new product, I implemented the following function that handled the upload of the product image:

const uploadImage = e => {
    let image;
    if(e.target.files[0]) {
      image = e.target.files[0];
      console.log(image);
    }
    const storage = fire.storage();
    const uploadTask = storage.ref(`images/${image.name}`).put(image);
    uploadTask.on('state_changed',
      snapshot => {
        const progress = Math.round((snapshot.bytesTransferred / snapshot.totalBytes) * 100);
        setProgress(progress);
      },
      err => {
        console.log(err);
      },
      () => {
        storage.ref('images').child(image.name).getDownloadURL().then(url => {
          console.log(image);
          setPic(url)
        })
      }
    )
  }

As with Auth and Firestore, working with Cloud Storage was just as easy and straightforward.

Cloud Functions

One of the main reasons I am so intrigued with Firebase is Cloud Functions. As a front-end developer cloud functions allows me to write event driven functionality on the server side without having to provision any infrastructure. For my exercise, I wanted to use Cloud Functions to interact with 3rd party services like MailChimp and Stripe so that I can implement ecommerce features within my app.

One of the best things I found working with Firebase and Cloud Functions was that I was able to focus on one feature and implement it end-to-end thus implementing the concept of micro-services. To start off, I was able to create a cloud function that would add a user to a MailChimp list. For this, I created another package in my project and initialised a Firebase Functions project. I then wrote a NodeJS function that I was able to deploy to Firebase which adds a new user to MailChimp using its REST API. The REST API also handles any errors like user already added to the list or malformed email addresses. The response is cascaded to the UI and is displayed accordingly.

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
admin.firestore().settings({timestampInSnapshots: true});
const cors = require('cors')({origin: true});
const Mailchimp = require('mailchimp-api-v3');
const mailchimp = new Mailchimp('XXXXXXXXXXX');
exports.addSubscriber = functions.https.onRequest((req,res) => {
  cors(req, res, async () => {
    const data = JSON.parse(req.body);
    const result = await mailchimp.post('/lists/XXXXXX/members', {
      email_address: data.email,
      status: 'subscribed',
      merge_fields: {
        FNAME: data.firstName
      }
    })
    .then((results) => {
      console.log('successfully added a new firebase user');
      return res.status(200).json({status: "200", message: "Successfully added a new user"});
    })
    .catch((err) => {
      console.log('Mailchimp: Error while attempting to add the user', err);
      return res.status(500).json({status: "500", message: "Error while attempting to add the user"});
    })
  });
});

The above is an example of a HTTPS function and there are many other triggers to make use of the event driven micro-service architecture. The full list is mentioned here. Firebase provides a rich dashboard for all its features and as an admin, I could monitor the health and execution of the cloud function. One of the features, I liked was the health section of the deployed functions which advises the most common and latest errors and failures of executing cloud functions along with the frequency of such events.

Error reporting of Cloud Functions for Firebase

As I was implementing a HTTPS Cloud Function, I was able to hook up the frontend quite easily using the standard fetch api within my React component as below:

import React, {useState} from 'react';
import styled from '@emotion/styled';
function Newsletter(props) {
  const Form = styled.form`
    display: grid;
  `;
  const Input = styled.input`
    font-family: 'Roboto Slab', serif;
    font-size: 18px;
    border: 2px solid #fff;
    border-bottom: 2px solid #000;
    margin-bottom: 20px;
    padding: 10px;
    &:focus {
      border: 2px solid #000;
      outline: none;
    }
  `;
  const Button = styled.button`
    border: 2px solid #000;
    background: #000;
    color: #fff;
    padding: 10px;
    font-family: 'Roboto Slab', serif;
    font-size: 18px;
    &:hover {
      background: #fff;
      color: #000;
    }
  `;
  const Message = styled.div`
    margin-top: 8px;
    border: 2px solid #3cae3c;
    color: #FFF;
    background: #3cae3c;
    padding: 8px;
    font-family: "Roboto Slab";
  `;
  const [message, setMessage] = useState(null);
  const handleSubmit = (e) => {
    e.preventDefault();
    const {name, email} = e.target.elements;
    const payload = {
      email: email.value,
      firstName: name.value
    }
    fetch('https://us-central1-ecommerce-8c3a8.cloudfunctions.net/addSubscriber', {
      method: 'POST',
      headers: {
        ContentType: 'application/json',
      },
      body: JSON.stringify(payload)
    })
    .then(response => {
      response.json().then(data => ({
        data: data,
        status: response.status
      })).then(res => {
        console.log(res);
        setMessage(res.data.message);
      })
    })
    .catch(err => {
      console.error("error adding user to the mailchimp list:", err);
    })
    document.getElementById("newsletter").reset();
  }
  return(
    <Form onSubmit={handleSubmit} id="newsletter">
      <Input type="text" placeholder="Your name" name="name" required />
      <Input type="text" placeholder="Your email" name="email" required />
      <Button>Subscribe</Button>
      {message && 
        <Message>{message}</Message>
      }
    </Form>
  )
}
export default Newsletter;

The above React component and the deployed Cloud Function is all that is required to implement a simple Newsletter Sign-up micro-service. This to me is the best feature of services like Firebase and serverless architecture. It allows the developer to concentrate on writing business features rather than mess around with infrastructure and server/networking setup.

Analytics for Firebase

The last feature I was able to test was integrating Google Analytics within the web app. This is one of the latest features released by Firebase (September 2019) and they couldn’t have made it easier to implement Google Analytics with a web app using Firebase. All you need is initialising the analytics in the config file:

import firebase from 'firebase/app';
import 'firebase/firestore';
import 'firebase/storage';
import 'firebase/auth';
import 'firebase/analytics';
const firebaseConfig = {
  apiKey: "XXXXXXXXXXXXXXXXXXXXXXX",
  authDomain: "ecommerce-8c3a8.firebaseapp.com",
  databaseURL: "https://ecommerce-8c3a8.firebaseio.com",
  projectId: "ecommerce-8c3a8",
  storageBucket: "ecommerce-8c3a8.appspot.com",
  messagingSenderId: "986285318316",
  appId: "XXXXXXXXXXXXX",
  measurementId: "G-63MZ8B7D74"
}
const fire = firebase.initializeApp(firebaseConfig);
firebase.analytics();
export default fire;
export const firebaseAuth = firebase.auth;

The above implementation allows me to implement some out of the box analytics events like first_visit, page_view, session_start etc.

Event logging in Firebase for out-of-the-box events
Analytics Dashboard on Firebase

Implementing custom events is possible & reports can be exported to Big Query for further analysis.

Multiple deployment environments

Implementing all these features within Firebase got me thinking about how would you manage modern development techniques like multiple environments (dev, stage and prod) and CI/CD. While there is no in-built functionality for having multiple deployable environments within the same project, one can create multiple Firebase projects and have a configuration file (.env) to have a similar functionality. For switching between environments when developing locally, you need to configure the environments using the Firebase CLI.

Adding and switching between environments with the Firebase CLI is as simple as one command: firebase use.

When you first initialise your Firebase Hosting project with firebase init you specify what project you want to deploy your app to. This is your default project. The use command allows you to add another project:

$ firebase use --add

You get presented with the existing list of projects and once selected you can assign an identifier for that project like staging or prod. Then in order to switch between environments, use the firebase use command alongwith the identifier to switch.

$ firebase use staging

Continuous Integration/Deployment

Implementing CI/CD is supported in a variety of ways. As I was using Bitbucket as my source control repository I was able to use a pre-built pipe to automatically deploy to Firebase when a commit was pushed to the Bitbucket repo. My bitbucket-pipelines.yml file implementation is as below:

image: node
pipelines:
  branches:
    master:
    - step:
        name: Build
        script:
          - npm install && npm run build
        artifacts:
          - public/**
    - step:
        name: Deploy to Firebase
        deployment: test
        script:
          - pipe: atlassian/firebase-deploy:0.2.1
            variables:
              FIREBASE_TOKEN: $FIREBASE_TOKEN
              PROJECT_ID: $FIREBASE_PROJECT

The above is a simple implementation where any commit to the master branch will result in a build of the project using the firebase pipe using the variables for Firebase token and the Project ID. The Firebase token can be generated for the user using the Firebase CLI.

Conclusion

My overall findings have been favourable to Firebase as it promotes the concept of rapid app development. This means that you can get started quickly. Since the services are all fully managed, scaling the project is not a problem though if not managed properly, the price of scaling can become high. While I only touched the surface of the mentioned Firebase services, we can see that Firebase services offer simple to use APIs and services to build web and mobile apps quickly which are not just prototypes and throw aways but actual production grade apps. In the end, I would like to continue expanding my ecommerce app and implement more Firebase services like Remote Config or implement machine learning capabilities like Predictions and hopefully share more in-depth notes on these services.

Leave a comment

Name*

Website

Comment