Firebase Functions

Learn how to integrate Doppler with Firebase Cloud Functions to sync secrets to production and during local development.

1328

In this guide, you'll learn how to integrate Doppler into a Firebase Cloud Functions application so you'll never have to maintain an .env file again.

πŸ“˜

Check out our Firebase sample repository to see a complete working solution.

Prerequisites

Secrets

Doppler syncs secrets to Firebase environment variables which are accessed from the doppler property returned by the functions.config() method:

const functions = require('firebase-functions');
const secrets = functions.config().doppler;

const API_KEY = secrets.API_KEY;

The following sections will now show you how to configure local development and CI/CD.

Local Development

Secrets are injected during local development using the CLOUD_RUNTIME_CONFIG environment variable populated by the Doppler CLI. This removes the need for .env and .runtimeconfig.json files altogether.

Configure the Doppler CLI to fetch secrets for the Development config by opening a terminal in the functions directory and running:

doppler setup

You can verify the secrets fetched by the CLI at any time by running:

doppler secrets

Now update the serve and shell scripts (or similar) in the package.json:

{
  "scripts": {
    "serve": "CLOUD_RUNTIME_CONFIG=\"$(doppler secrets download --no-file | jq '{doppler: .}')\" firebase emulators:start --only functions",
    "shell": "CLOUD_RUNTIME_CONFIG=\"$(doppler secrets download --no-file | jq '{doppler: .}')\" firebase functions:shell",
  }
}

Then test the functions emulator by running:

npm run serve

You and your teammates will never have out-of-date secrets again plus you don't have to worry about leaking credentials in unprotected .env or .runtimeconfig.json files.

CI/CD

Deploying to production in CI/CD is a two-step process:

  1. Update the function environment variables
  2. Update the function code

Add a new secrets-sync script to the package.json and update the existing deploy script to use it:

{
  "scripts": {
    ...
    "secrets-sync": "firebase functions:config:unset doppler && firebase functions:config:set doppler=\"$(doppler secrets download --no-file)\"",
    "deploy": "npm run secrets-sync && firebase deploy --only functions",
  }
}

Your CI/CD environment will need a Doppler Service Token injected via a DOPPLER_TOKEN environment variable to provide read-only access to the Production config.

A Firebase deploy GitHub Action would then look something like this:

name: deploy

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./functions
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
      - uses: dopplerhq/cli-action@v1
        with:
          node-version: '16'
      - run: curl -sL https://firebase.tools | bash
      - run: npm install
      - run: npm run deploy
        env:
          DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }}
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}

Troubleshooting

If you're encountering trouble involving functions.config().doppler being undefined when you're trying to access variables (typically when attempting to initialize your Firebase application and are passing in database credentials or similar), then you're likely running into a problem that was introduced with changes in [email protected]. The underlying issue is discussed in this GitHub issue and further discussed in this issue. Unfortunately, there isn't a fantastic workaround currently. The problem only seems to impact local development. Your options are:

  1. Downgrade to [email protected] until they decide to make changes (please add your feedback to this GitHub issue!).
  2. Change how your application works such that your application initialization occurs in function calls.

Method two will be discussed below.

Local Development Initialization Workaround

This workaround involves creating a singleton class that performs your application initialization. This singleton then needs to be instantiated in every function you have (obviously not ideal, but is currently the only known workaround for this aside from downgrading firebase-tools). The singleton class might look something like this:

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

class PrivateFirebase {
  constructor() {
    const dbCredentials = {
      type: process.env.FIRE_BASE_DB_CREDENTIALS_TYPE,
      project_id: process.env.FIRE_BASE_DB_CREDENTIALS_PROJECT_ID,
      private_key_id: process.env.FIRE_BASE_DB_CREDENTIALS_PRIVATE_KEY_ID,
      private_key: process.env.FIRE_BASE_DB_CREDENTIALS_PRIVATE_KEY,
      client_email: process.env.FIRE_BASE_DB_CREDENTIALS_CLIENT_EMAIL,
      client_id: process.env.FIRE_BASE_DB_CREDENTIALS_CLIENT_ID,
      auth_uri: process.env.FIRE_BASE_DB_CREDENTIALS_AUTH_URI,
      token_uri: process.env.FIRE_BASE_DB_CREDENTIALS_TOKEN_URI,
      auth_provider_x509_cert_url:
        process.env.FIRE_BASE_DB_CREDENTIALS_AUTH_PROVIDER_X509_CERT_URL,
      client_x509_cert_url: process.env.FIRE_BASE_DB_CREDENTIALS_CLIENT_X509_CERT_URL,
    };

    this.app = admin.initializeApp({
      credential: admin.credential.cert(dbCredentials),
      databaseURL: process.env.FIRE_BASE_DB_CREDENTIALS_DB_URL,
    });

    this.db = admin.firestore();
    this.storage = admin.storage();
  }
}

class Firebase {
  constructor() {
    throw new Error('Use Firebase.getInstance()');
  }

  static getInstance() {
    if (!Firebase.instance) {
      Firebase.instance = new PrivateFirebase();
    }
    return Firebase.instance;
  }
}

exports.Firebase = Firebase;

You would then use it something like this:

const { Firebase } = require("./firebase");

exports.helloWorld = functions.https.onRequest((request, response) => {
  const fb = Firebase.getInstance();
  functions.logger.info("Hello logs!", {structuredData: true});
  response.send(`Doppler Project: ${process.env.DOPPLER_PROJECT}, Doppler Config: ${process.env.DOPPLER_CONFIG}`);
});

Every function would need to have the const fb = Firebase.getInstance(); line added to it since calling that outside of a function results in it being instantiated before functions.config() or process.env have been populated due to how Firebase does this now.

πŸ‘

Amazing Work!

Now you know how to use Doppler to manage and sync secrets for Firebase Cloud Functions in local development and production.