Skip to content

Instantly share code, notes, and snippets.

@34fame
Last active April 15, 2021 03:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 34fame/94dcb0a548d4e827fd771294113b62cf to your computer and use it in GitHub Desktop.
Save 34fame/94dcb0a548d4e827fd771294113b62cf to your computer and use it in GitHub Desktop.
Real World Firebase Setup

Setup

Install Packages

# Might require sudo
npm i -g firebase-tools

Project Directories

Create a project directory with a child client and server directory.

# Project directories
project
  L client
  L server

Initialize Client

The following commands will perform the basic setup for hosting our client in Firebase.

cd client
yarn init -y

# Connect to Google account
firebase login

# Initialize Firebase client
firebase init
# Select `Hosting` and `Emulators`
# Select project or create a new project.  This should be a dev or sandbox project.  We will add a prod setup later.
# Set public directory to path of `index.html`
# Select `Yes` to rewrite all urls (assuming this is an SPA)
# Select `No` for automatic builds and deployes with GitHub
# Select the `Hosting` emulator
# Keep the default port
# Select `Y` to enable Emulator UI
# Set the Emulator UI port to `4001`
# Select `N` to download the emulators

After the commands are run, you should end up with the following directories and files.

project
  L client
    L .firebaserc
    L .gitignore
    L firebase.json
    L package.json
    L public
  L server

Intialize Server

The following commands will perform the basic setup for hosting our server and enabling other Firebase services.

cd server
firebase init
# Select `Firestore`, `Functions`, and `Emulators`
# Select same project as the client
# Keep default file names
# Select `JavaScript`
# Select `N` for ESLint
# Select `N` to install dependencies
# Select `Authentication`, `Functions`, `Firestore`, and `Pub/Sub` emulators
# Change Firestore port to `8070` but leave the rest as default
# Select `Y` to enable the Emulator UI
# Set the Emulator UI port to `4000`
# Select `N` to install emulators

cd functions
yarn add firebase-admin

# If you get the error: 'The engine "node" is incompatible with this module...', run the following commands
yarn config set ignore-engines true
yarn add firebase-admin

After the commands are run, you should end up with 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

Client Emulators

Verify with the Firebase Hosting emulator.

cd client
firebase emulators:start
  • Open http://localhost:5000 in your browser.
  • You should see a page showing the site is up and running the Firebase SDK.
  • You can close the emulator using ctrl-c / cmd-c

Server Emulators

Verify with the Firebase Authentication, Functions, Firestore and Pub/Sub emulators.

  • Open server/functions/index.js
  • Uncomment the export
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!");
});
  • Launch emulators
cd server/functions
firebase emulators:start

Development

With our client and server emulators operational, we have everything we need to build our entire app without the costs or delays of using the actual cloud services.

This 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.

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

Sandbox Deployment

Remember that are not using a production environment yet. Right now we only have a "sandbox" project in Firebase.

Deploy Client

cd client
firebase deploy
  • Yep, that's it!
  • At the end of the output you will see a message with the "Hosting URL". Open this URL in the browser and you will see the same page as before, only this time it is hosted in the Firebase cloud.

Deploy Server

Before Firebase will allow us to deploy to Functions, we must upgrade out "Spark" plan to the "Blaze" plan. This enables the possibility of billing for usage. I can tell you that, in my experience, I have never owed more than $2 in a month and its normally under $1.

cd server/functions
firebase deploy
  • The deploy will take a while but you will see the URL (endpoint) to the Functions server API (HelloWorld).
  • Instead of the Emulator UI, you can now use the Firebase Console

If this were a complete app, we could now provide our users/testers with the Hosting URL and let them do their thing. In the meantime we can continue with any development locally.

** NOTE: ** When working in a team, everything we've discussed is the same. The only difference is you'll want to use GitHub (or equivalent) so everyone has a copy of the project locally and where they can also run the emulators. If someone clones your project, they will still need to perform the Firebase initialization steps (firebase login one time and firebase init in both the client and server directories.

Testing

As developers, we all know we need to do testing. Whether you use Jest for unit testing or Cypress for end-to-end testing, the point is, we need to do it.

The emulated environment we have is actually excellent for testing. There are some things we just can't do when we have services running in the public cloud. With our emulators though, we can test whatever we want.

The Firestore emulator is especially interesting. By default, every time you load the emulator, the Firestore repository is completely empty. That's actually a great thing because deleting a lot of data from Firestore in the cloud is not the easiest thing to do. So, every time you load up the emulators, you know you are starting with a clean environment.

However, there is a convenient way to load data at start-up. First, we need data. Using the Firebase CLI we can export data straight out of our Firestore emulator. So we start up the emulators. Add data either via our client application or perhaps using the API endpoints. Before shutting down the emulators we run an export.

cd server
firebase emulators:export ./export

This creates several files and directories that appear to be binary in nature.

Now we can shut down the emulators. When we start them back up we need to tweak our start command.

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

You can even make sure you refresh your export every time you stop the emulator.

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

While the emulators are running you can also quickly clear Firestore using the Emulator UI. When you go to the Firestore tab there is a button to "Clear All Data". This is very handy when you trying to debug something that requires constant clearing of data.

Let's say we now have our MVP product ready to go. It's been tested and our stakeholders have approved. The next step is to put this baby into production!

What are our options? First off, we could just say what we have is production and just to development and testing with emulators. The problem is, not everything works in emulators. If you integrating with other external services (e.g. Stripe, SendGrid, Algolia, etc.) you need to be able to perform system testing.

At a minimum, we need to create one additional Firebase project that will act as production. You can certainly add more and the next steps will just need to be repeated.

Production

With our current setup, we have a single code project with a client and server directory. We are using git and GitHub so we are able to share code, create branches, and the like. So, there is not a good reason to create a new code project for production. It creates a lot of overhead we don't need.

First thing we need to do is create the new project in Firebase. Simple.

Preparation

Firebase Configuration

Now we need to let our client and server setup know about our new environment. We do this using the Firebase tools.

For clarity sake, let's say our current project is called "beefcake-staging" and we just created "beefcake-prod".

cd client

firebase use
# Displays a list of known Firebase projects and shows which project is currently active
# In our case we should see a single project entry
# * default (beefcake-staging)
# The "*" indicates active.  `default` is an alias.  `beefcake-staging` is the Firebase project name.

# To assign the active project we use its alias
firebase use default

# To add our newly created project
firebase use --add
# Select existing project
# Select `beefcake-prod`
# Set the alias to `prod`

cd ../server
firebase use --add
# Select the `beefcake-prod` project
# Set the alias to `prod`

So, why does this matter? When we use the firebase deploy command, or other Firebase tool options for that matter, it will attempt to deploy our client to whatever the currently active project is. For Firebase, this is all we have to do in order to switch project context.

Client Code Changes

If we had an actual client application, we would have stored variables for how to connect to Firebase and any other service we are connecting to. So, now that we are moving to a new Firebase project, how to we accomodate that change in our code?

There is more than one answer, but let's keep it simple. Many times we need to hide keys and we don't want those keys stored in our code anyway. Instead, you would use a library like dotenv.

project
  L client
    .env.staging
    .env.prod
  L server
    L functions
      .env.staging
      .env.prod

Use dotenv and create a .env.<environment> file for each environment. When you load dotenv you can dynamically specify the file, or, copy the appropriate env file to .env in your build scripts.

require('dotenv').config({ path: `.env.${process.env.NODE_ENV}` })

Package Scripts

Let's look at how we can make life easier by implementing some scripts in our package.json files. First off, I want to launch a single script from the project root directory and have it run the appropriate scripts in the client and server/functions directories.

cd <project>
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

Now for the scripts. When done, I will only need to run one command from the project directory to either run emulators, deploy to staging or deploy to production.

# Run emulators
yarn serve:local

# Deploy to staging
yarn deploy:staging

# Deploy to prod
yarn deploy:prod

NOTE: I'm only going to include the scripts portion of the package.json files!

Project Scripts

{
  "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 Scripts

{
  "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 Scripts

{
  "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"
  }

Conclusion

It took me several months to get these steps figured out. There was a lot of trial-and-error along the way. During my research I found that many others were unsure how to take their Firebase setup to the next level with multiple environments. I found very few answers. Hopefully you found this article at the perfect time and it saved you weeks of frustration! Or, if its already been weeks, maybe this article gives you hope that it can be done!

There are so many courses and tutorials on building a project with Firebase. I wanted this walk-thru to continue where those instructions ended. Please let me know if I nailed it, or whiffed it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment