The Missing Chapter for Building Applications on Firebase: Create Different Firebase Environments

How to create different environments for testing, staging, and production

Troy Moreland
Level Up Coding

--

You finally bought that course on Udemy to learn Firebase. After all, the price dropped to $11.99! Ok, maybe it was a free YouTube tutorial. Whatever it was, I’ll assume you now understand how to use Firebase services like Hosting, Authentication, Firestore, Functions, and Storage. You even built this amazing To-Do app running solely on Firebase services!

Photo by Austin Schmid on Unsplash

Now a new question comes to mind. How do I implement my SDLC process on Firebase?

I need to have separate environments for testing, staging, and production.

I had the same question after the initial euphoria wore off of seeing my data in Firestore and my files in Storage. For me, the solution was not trivial. I want to share with you how I was finally able to come up with a setup that I’m truly happy with.

This tutorial is focused on the configuration of your full-stack application to support concepts of development, testing, staging and production. This tutorial will not cover application code or how to use the Firebase SDKs.

Prerequisites

Due to the topic of this tutorial, I will be making the following assumptions:

  • You are a Javascript developer
  • You are comfortable with Node.js, NPM, and Yarn
  • You are at least familiar with Firebase
  • You are comfortable with git and GitHub

Setup

This section might seem trivial if you feel comfortable with Firebase but I wanted to start from the beginning. A fresh project. Let’s call it, “beefcake”. Yes, that was the first thing that popped into my head. Weird.

Let’s start with directory structure. Since we are building a full-stack application, I create the following structure. If I don’t specify a working path, assume that you are sitting in the project directory, “beefcake”.

beefcake
L client
L server

Make sure you have the Firebase tools installed.

> npm install -g firebase-tools

If you haven’t used Firebase tools on your device yet, you will need to perform the one-time login.

> firebase login

Client Configuration

cd client
yarn init -y
firebase init
  • Select “Hosting” and “Emulators”
  • Create a “sandbox” project (e.g. beefcake-sandbox)
  • Keep default public directory
  • Select “Yes” to rewrite all urls
  • Select “N” to automate builds
  • Select “Hosting” emulator
  • Keep the default port
  • Select “N” to enable Emulator UI
  • Select “N” to download emulators

Server Configuration

cd server
firebase init # See answers below and then continue here
cd functions
yarn add firebase-admin
# If you get a Node engine error...
yarn config set ignore-engines true
yarn add firebase-admin
  • Select “Firestore”, “Functions”, and “Emulators”
  • Select the existing project (e.g. beefcake-sandbox)
  • Keep default file names
  • Select “JavaScript”
  • Select “N” to ESLint
  • Select “N” to install dependencies
  • Select “Authentication”, “Functions”, “Firestore”, and “Pub/Sub” emulators
  • Set Firestore port to “8070” but leave the rest as default
  • Select “Y” to enable Emulator UI
  • Set Emulator UI port to “4000”
  • Select “N” to download emulators
  • Return to code block above to finish commands…

Now our project should have the following directories and files.

project
L client
L .firebaserc
L .gitignore
L firebase.json
L package.json
L public
L server
L .firebaserc
L .gitignore
L firebase.json
L firestore.indexes.json
L firestore.rules
L functions
L .gitignore
L index.js
L node_modules
L package.json
L yarn.lock

Verify Setup

First let’s verify the client configuration by running the Hosting emulator.

cd client
firebase emulators:start

Open http://localhost:5000 in your browser. You should see a Firebase page and text indicating the Firebase SDK is in use. You can now stop the emulator with Ctrl-c.

Before we start the server emulators we need to uncomment some default code. In your editor, open beefcake/server/functions/index.js. Uncomment the helloWorld function.

const functions = require("firebase-functions");

// // Create and Deploy Your First Cloud Functions
// // https://firebase.google.com/docs/functions/write-firebase-functions
//
exports.helloWorld = functions.https.onRequest((request, response) => {
functions.logger.info("Hello logs!", {structuredData: true});
response.send("Hello from Firebase!");
});

Now launch the emulators.

cd server/functions
firebase emulators:start

Open http://localhost:4000 in your browser. You should see the Firebase Emulator Suite page which shows our services running. Open http://localhost:5001/beefcake-sandbox/us-central1/helloWorld in your browser (your project and zone is likely different but the rest should be the same). You should see “Hello from Firebase!”. This indicates our endpoints served by Functions are working. You can close these emulators now.

Development

With our client and server emulators operational, we have everything we need to develop our full-stack app, without the costs or delays of using the live cloud services.

As I said before, the focus of this tutorial is not how to develop your app. We are focused on the approach for moving through our development lifecycle when using Firebase. So let’s fast forward a bit.

At this point, let’s assume we are “done” with development and we are ready to deploy what we have to Firebase so that others can access the application.

Sandbox Deployment

Remember that we are not using a production environment yet. Right now we just have our sandbox. We just need to deploy our client and server projects into that environment.

Let’s start with the client project.

cd client
firebase deploy

Done! For real! Once the deploy is done, you will find a “Hosting URL” on the screen. If you open your browser to that URL, you should see the same page we saw before, only this time, it is being served to us from Firebase.

The server deployment has one “gotcha”. You have to upgrade your Firebase project from “Spark” to “Blaze”. The “Spark” plan is free, with limitations. The “Blaze” plan provides free usage limits and then bills you for overages. Look over the pricing to make sure you understand the risks. I personally have never had a bill over $2/month and normally it’s less than $1/month.

As soon as you upgrade, it’s time to deploy!

cd server
firebase deploy

This deployment will take longer than the client. When done, you will see a URL for your “helloWorld” endpoint. Instead of the Emulator Suite, you will now use the Firebase console. As long as the “helloWorld” URL works, we are done with the sandbox deployment.

At this point, you would give stakeholders the client URL and let them experience your glory.

When working with a team, all of this still applies. Each developer would have a copy of the code (e.g. git). If someone cloned the repository for the first time, they would need to run the firebase init in the client and server directories before emulators or deployments will work.

Testing

I am not going to cover how you do testing for your application. I simply want to touch on a couple of key points on how Firebase plays into your testing.

The emulator suite is actually fantastic for testing. For one, you aren’t limited on what you can test, unlike when your application is running in Firebase. Another great thing about the emulators is when you stop and start them again, everything is clean. As in, empty. There is no tool for clearing out data in Firestore. It is not fun during development or testing to deal with your data in the live services. Not with emulators!

But… What if you do not want to lose your data? What if you need to preload data for testing? Once again, emulators to the rescue! While your emulators are running, you can export the data.

cd server
firebase emulators:export ./export-dir

This performs a dump that can then be imported. You can also instruct the emulators to always to an export before shutting down.

cd server
firebase emulators:start --import=./export-dir --export-on-exit

You will still need to perform some system testing with your application running in Firebase but, for unit and e2e testing, the emulator suite is very handy.

Production

Now we are finally getting to the gist of this tutorial. We have handled development, testing, and even provided a sandbox for stakeholders.

How do we roll this into production and still keep what we have for future development, testing, etc.?

The good news is, our code and where it lives doesn’t need to change. We don’t need to make a new project code folder. We don’t need to run parallel git branches or anything of the sort.

Long story short, we just need to tell our client and server configurations about the new Firebase project and then be able to set the current project on demand.

cd client# Display a list of known projects
> firebase use
* default (beefcake-sandbox)
> firebase use --add

First we used firebase use to see what is currently configured. We can see we have one project configured (“beefcake-sandbox”) with an alias of “default”.

Next we ran firebase use --add. This tells Firebase we want to add another project. So, create the production project (e.g. “beefcake-prod”) and assign it the alias “prod”.

Now we just need to do the same in the server project.

cd server
firebase use --add

Be sure to select the existing production project we just created.

From now on, if we want to deploy our code to production, we need to run firebase use prod first. This sets the context for the Firebase tools.

Environment Variables

In your application code you will have environment specific variables that also need to be updated when you change between sandbox and production. There are numerous methods to achieve this. I am not suggesting one method over another, but, because it is applicable to this topic, I will show you one way.

Most of the time I use the dotenv library to manage environment variables. This allows you to put sensitive tokens in separate files and then exclude them in any public repo with your .gitignore file. As such, you would normally create a .env file in your client and server projects. I also create one for each environment. Later we will script copying them on demand.

beefcake
L client
.env # used by dotenv
.env.local # copy to .env when running emulators
.env.sandbox # copy to .env on sandbox deploy
.env.prod. # copy to .env on prod deploy
L server
L functions
.env
.env.local
.env.sandbox
.env.prod

Package Scripts

The final piece to this configuration is to simplify things with scripts in our package.json files. We want to make sure we are setting the environments properly while making things really simple to use.

To this point we haven’t done anything in our project root folder, other than creating the client and server folders. We are going to run our scripts from here which, in turn, will run the proper scripts in the client and server areas.

We need to initialize the project directory to use scripts and then add a helpful library called npm-run-all to all three spots where we have a package.json file.

yarn init -y
yarn add -D npm-run-all
cd client
yarn add -D npm-run-all
cd ../server/functions
yarn add -D npm-run-all

Project Directory Scripts

Here is an example set of scripts that would be kept in the project directory’s package.json file. I’m excluding irrelevant portions of that file so don’t take this contents as literal.

{
"name": "project",
"scripts": {
"serve:local": "npm-run-all --parallel client:local server:local",
"deploy:staging": "npm-run-all client:deploy:staging server:deploy:staging",
"deploy:prod": "npm-run-all client:deploy:prod server:deploy:prod",
"client:local": "cd client && yarn serve:local",
"client:deploy:staging": "cd client && yarn deploy:staging",
"client:deploy:prod": "ce client && yarn deploy:prod",
"server:local": "cd server/functions && yarn serve:local",
"server:deploy:staging": "cd server/functions && yarn deploy:staging",
"server:deploy:prod": "cd server/functions && yarn deploy:prod"
}

Client Directory Scripts

The path is client/package.json.

{
"name": "client",
"scripts": {
"serve:local": "npm-run-all use:staging copy-env:local start:local",
"deploy:staging": "npm-run-all use:staging copy-env:staging quasar:build:staging firebase:deploy:staging",
"deploy:prod": "npm-run-all use:prod copy-env:prod quasar:build:prod firebase:deploy:prod",
"use:staging": "firebase use default",
"use:prod": "firebase use prod",
"copy-env:local": "cp .env.local .env",
"copy-env:staging": "cp .env.staging .env",
"copy-env:prod": "cp .env.prod .env",
"start:local": "firebase emulators:start",
"firebase:deploy:staging": "firebase deploy -P default",
"firebase:deploy:prod": "firebase deploy -P prod"
}

Server Directory Scripts

The path is server/functions/package.json.

{
"name": "server",
"scripts": {
"serve:local": "npm-run-all use:staging copy-env:local start:local",
"deploy:staging": "npm-run-all use:staging copy-env:staging firebase:deploy:staging",
"deploy:prod": "npm-run-all use:prod copy-env:prod firebase:deploy:prod",
"use:staging": "firebase use default",
"use:prod": "firebase use prod",
"copy-env:local": "cp .env.local .env",
"copy-env:staging": "cp .env.staging .env",
"copy-env:prod": "cp .env.prod .env",
"start:local": "firebase emulators:start",
"firebase:deploy:staging": "firebase deploy -P default",
"firebase:deploy:prod": "firebase deploy -P prod"
}

Execution

Typically you will start things from the project directory. Your options are this point are to run locally by launching all emulators, deploy client and server to the sandbox environment, or deploy client and server to production. As you travel the path of the scripts you will notice there are multiple actions taken to make sure the environments are setup just right for what we want to accomplish. To do all of this by hand would be asking for trouble.

# Run locally with emulators
yarn serve:local
# Deploy to sandbox
yarn deploy:sandbox
# Deploy to production
yarn deploy:prod

Conclusion

It took a lot of research and trial-and-error before I felt happy with this setup. During my research I found many others asking how to do something like this but I never found an answer that was either complete enough or clear enough for me.

There are so many courses and tutorials on how to use Firebase in your projects but I haven’t found a single one that handled the need for multiple environments. I wanted this walk-thru to continue where those instructions ended. Please let me know if I nailed it, or whiffed it!

--

--

Started career as a developer in the Marine Corps in 1991. Founded Identity Automation in 2004. Always learning. Always coding. What's next?