← Back to blog

How to deploy Functions to Firebase from a mono repo

Posted on August 07, 2020

Monorepos are gaining popularity every day. Although the advantages of having a monorepo are huge, the maintenance and tooling are not there yet.

I use Firebase often in my projects. It allows me to put the idea out quickly and iterate efficiently. Though, when I introduced monorepo into the code base, I had issues setting up a clean development cycle. Firebase tools still not designed for monorepo setup. There are several approaches you can make it work. In this post, I will be talking about how I set up my projects step by step.

I assume you already have a mono repo setup with yarn workspaces and Lerna. We will be adding a functions package to the repo and will be using a shared module with functions package.

Here is an example for the directory structure of a mono repo.

|--
|-- packages
|-- |-- shared
|-- |-- |-- package.json
|-- |-- functions
|-- |-- |-- package.json
|-- |-- web-app
|-- |-- |-- package.json
|-- package.json

Step 1: Initialize Firebase

The fastest way to get started is to initialize Firebase in a separate folder. Use the “firebase init” command to set up the base project with functions. I found it best to let CLI scaffold the project and configure afterward inside the mono repo.

By default, Firebase initializes going to create functions into a separate folder and put the configuration files in the root directory. Yarn workspaces do not support more than one level of packages. The first thing to do is to carry the configuration files to the functions directory. Move the functions directory under the packages folder. You will stumble with your first error as the required engines field in package.json file will not allow you to install dependencies with yarn if you are not using node version 10.

It is an easy fix. Just need to add the --ignore-engines command to install. To make it as default behavior, you can add a .yarnrc file to your root directory with the following snippet.

--ignore-engines true

We are not ready yet, but at least now we can install the dependencies. The next step is the configuration.

Step 2: Functions Package Configuration

Firebase expects node modules to be in the root directory of functions. On the other side, yarn workspaces save all dependencies to the root of the mono repo. The first step is to do is to tell yarn to move those dependencies to functions package. Update the functions package.json workspaces field, with all the dependencies that it is using.

{
  "name": "@my-project/functions",
  "version": "0.0.0",
  "scripts": {
    "build": "tsc",
    "serve": "tsc && firebase emulators:start --only functions",
    "shell": "tsc && firebase functions:shell",
    "start": "yarn run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "engines": {
    "node": "10"
  },
  "main": "lib/index.js",
  "workspaces": {
    "nohoist": [
      "**/@my-project/*",
      "**/firebase-admin/*",
      "**/firebase-functions/*"
    ]
  },
  "dependencies": {
    "firebase-admin": "^8.10.0",
    "firebase-functions": "^3.6.1"
  },
  "devDependencies": {
    "typescript": "^3.8.0",
    "firebase-functions-test": "^0.2.0"
  },
  "private": true
}

The last step for the configuration is to tell Firebase where to look for the source code. By default, Firebase expects to find the functions under the “functions” folder. Update Firebase.json file functions field to point to the correct directory (which is root directory). Here is an example of the Firebase.json file.

{
  "firestore": {
    "rules": "firestore.rules",
    "indexes": "firestore.indexes.json"
  },
  "functions": {
    "source": ".",
    "predeploy": [
      "yarn run build"
    ]
  },
  "storage": {
    "rules": "storage.rules"
  },
  "emulators": {
    "functions": {
      "port": 5001
    },
    "firestore": {
      "port": 8080
    },
    "pubsub": {
      "port": 8085
    },
    "ui": {
      "enabled": true
    }
  }
}

As a tip, you can use the firebase init firestore command initialize and deploy security rules and indexes from your local development environment.

As long as you are not importing other packages from the mono repo, functions are ready to develop and deploy. In the next step, we will tackle how to use local packages with google functions.

Step 3: Adding Local Packages

When you deploy functions to Google Cloud, a remote server builds them from scratch. So while in the local development environment, mono repo packages served from node_modules directory, after deployment, the server tries to install them from npm. You need a way for the server to access the source code. You have a couple of options:

I love to keep things simple. Setting up a publishing flow is not that simple to scale. Furthermore, it creates a maintenance load for the published package. Especially if you automate the deploys via node scripts, packing the library makes much more sense.

Here are the steps to deploy google functions with a local package.

  1. Pack the library with the yarn run pack command.
  2. Move the tarball file to the functions root directory.
  3. Use the tarball file instead of the local package.

You can automate these steps via node or bash script. The only problem with this is, the linking won’t work with tarball files. So I generally prefer to use the local package directly during development. During the deployment, a node script packs the local library, move to the directory, and updates the package.json. After successfully deploy, reverts the package.json dependencies.

One last tip is to not to depend on many local dependencies. I generally try to keep it to only one package. You can create a package that abstracts the needed functionality from several packages.

Final Words

The Mono repo concept is still new for smaller development teams. It is not always easy to set up and maintain a mono repo. As long as the team is aware of the building process of their codebase, plan carefully, and design the architecture accordingly, everything will go smoothly.