Continuous integration and deployment workflow with Vercel, Firebase and CircleCI

8 feb 202110 min read

I've recently set up the whole CI/CD workflow for a project at work, this was the first time I was really diving into CI configuration and automated deployment with this stack. With the combination of the tools below, I feel we have achieved the right mix of confidence / automation for our current needs.

Tools and their responsibilities

  • CI : We use CircleCI for running all our tests / linting / type checking for both front end and backend. It also orchestrates backend deployment for Firebase function
  • Vercel : We use Vercel to deploy our front end. It is linked to our github repo to automatically deploy the front-end.
  • Firebase : We use Firebase for the backend (serverless functions as well as database and authentication). Generally we'll use one project for development and one project for prod.

Overview

Continuous integration and deployment with Firebase, Vercel and circleCI

Front-End

Our client is basically a NextJs React app. It pairs really well with Vercel since they are the ones behind NextJs, the integration / tooling is really fluid.

To get started you'll need to:

This should be enough to get you started and being able to deploy automatically whenever you merge on your configured branch on vercel.

If you need to customize the build-step, whereas it is the command or the output folder, you can follow their documentation

Environment variable

All your environment variables can be set up directly on Vercel

If you need to provide different values in your codebase based on the branch you're on (and therefore whether you target your staging or prod domain) you can rely on the System Env Variables

export const getEnvVar = (key: string): string => {
    const isProd = process.env.NEXT_PUBLIC_VERCEL_GITHUB_COMMIT_REF === 'master';

    switch (key) {
        case 'NEXT_PUBLIC_FIREBASE_API_KEY':
            return isProd
                ? process.env.NEXT_PUBLIC_PROD_FIREBASE_API_KEY
                : process.env.NEXT_PUBLIC_FIREBASE_API_KEY;
        default:
            throw new Error(`key ${key} is not recognized `);
    }
}

Note on firebase hosting : To deploy it we were initially using firebase hosting, but it was rather slow as you need to rely on a serverless function from firebase to serve the server side content.

If you want to use Firebase Hosting you would be limited to a function in us-central1. This is a known limitation and for the meantime if you want to serve dynamic content you would have to configure Google Cloud Run instead

Back end - firebase serverless functions

This part is a bit trickier between local and prod setup. There are a few firebase key concepts behind it.

  • Firebase project : usually you use a firebase project for your prod, and one for your staging. Meaning that you will need to switch between project whenever you want to deploy code on staging or on prod. This will be automated through CI.
  • Service account : This is one of the way to authenticate yourself as an admin of a firebase project. Your service account can be generated in the firebase console
  • RuntimeConfig : this is the config you can set up through firebase CLI usually to add env variables associated to your firebase project.
  • Firebase CLI : The command line tool which allow you to perform several tasks on firebase like :
  • login into your firebase project.
  • adding/selecting the project you're currently working on
  • adding env variable to your project
  • generate the firebaseToken needed to deploy through the CI

We'll go through each of them in the setup below, don't worry (I'm mostly speaking to myself for the next time I need to configure this 😄)

Setup commmon to both local and staging/prod

Firebase Project Configuration :

  • You'll therefore first need access to one or two firebase projects. This setup will mostly walkthrough having 2 firebase projects (staging and prod) with each their own authentication and database. Just log on firebase and follow the steps
  • Install firebase-cli
npm install -g firebase-tools
  • Login through firebase-cli. This allows the next steps
firebase login
  • Once you're logged in you'll be able to add different firebase projects to your current configuration by :
firebase use --add

This should result in .firebaserc file in your project where you'll have a list of your projects. This is important as we'll rely on those aliases to target the right project to deploy based on the branch later on. Would you have any issue you can refer to firebase-cli documentation which rather good.

Environment variable :

For your env variables you can add them directly to your firebase project through firebase CLI :

firebase functions:config:set someservice.id="THE CLIENT ID"

You can later on access it through :

const functions = require('firebase-functions');
...
const id = functions.config().someservice.id,

Note : Those environment variables are stored directly in your project on firebase and as such you don't need them locally. Usually you download them locally to verify a value manually. Might be a good idea to gitignore them, just in case.

Initialize admin SDK :

To configure and add your firebase-admin sdk, you'll need to add a service account.

This will result in a local .json file, that you will place under a specific gitignore folder on your machine.

Firebase then recommends putting the path of the file in an environment variable (previous section) - you can refer to the firebase docs if needed

import * as admin from 'firebase-admin';
...
const config = functions.config();

const app = admin.initializeApp({
            credential: admin.credential.cert(config.credentials.google_application),
            databaseURL: `https://${config.credentials.project_id}.firebaseio.com`
        });

Local Setup

To develop locally, usually I end up using the staging project for database/auth and serverless function. This can be achieved by :

  • select your firebase project manually through firebase CLI with firebase --use XXX

Note : Pay attention to which project you're currently using, sometimes you want to run a script against prod and you end up using firebase --use master which will be the selected project untill you reselect staging / your test env

We want to avoid deploying the serverless functions manually at this stage and want to pass this responsibility to the CI

CI Setup

The CI will be responsible for deploying our functions to firebase.

There is obviously some constraints :

  • you don't want to expose your serviceAccount in your repo
  • you need to switch the project used to deploy based on the branch

To achieve those goals we have to configure a few things :

  • take the path to where your service account is supposed to be stored and save it as an env variable on CircleCI.
  • take your local serviceAccount and encode it to base64 to save it as an env variable on circleCI. This allows CircleCI to store complex structure as a string.
  • a good practice is to add BASE_64 at the end of your variable name. Usually I end up with 2 var in circleCI, something like : FUNCTION_PROD_SERVICE_ACCOUNT_BASE64 and FUNCTION_STAGING_SERVICE_ACCOUNT_BASE64
  • take the serviceAccount from circleCI and decode/copy it through the CI to your build folder :
command: echo << parameters.key_env_name >> | base64 -di > ./keys/<< parameters.key_path >>
  • Now your code should be ready to be deployed, but you still need to make sure you have the token to allow your CI to handle the deployment. This can be done with the firebase token. This is done only once - you don't need to do it each time you switch the project you're using.
  • From there you should be able to deploy your serverless functions based on your branch like so :
command: ./node_modules/.bin/firebase deploy --token "$FIREBASE_TOKEN" -P << parameters.environment >> --only functions:hubspot,functions:users

This can only be triggered based on the branch you're running the merge against. Like so

- functions_deploy:
    requires:
      - functions-build
    environment: staging
    key_path: $STAGING_SERVICE_ACCOUNT_PATH
    key_env_name: $FUNCTION_STAGING_SERVICE_ACCOUNT_BASE64
    filters:
      branches:
        only: develop

- functions_deploy:
    requires:
      - functions-build
    environment: prod
    key_path: $PROD_SERVICE_ACCOUNT_PATH
    key_env_name: $FUNCTION_PROD_SERVICE_ACCOUNT_BASE64
    filters:
      branches:
        only: master
functions_deploy:
  executor: node
  parameters:
    environment:
      type: string
    key_path:
      type: string
    key_env_name:
      type: string
  working_directory: ~/project/app/functions
  steps:
  - checkout:
      path: ~/project
  - attach_workspace:
      at: ~/project
  - run:
      name: add service account
      command: echo << parameters.key_env_name >> | base64 -di > ./keys/<< parameters.key_path >>
  - run:
      name: Firebase Deploy
      command: ./node_modules/.bin/firebase deploy --token "$FIREBASE_TOKEN" -P << parameters.environment >> --only functions:hubspot,functions:users

That should be it and you should now have a fully automated deployment with Vercel and Firebase for both the front end and backend on your staging and prod environment.

I hope this has been helpful to you and that you learned a few things along the way, I surely did !