Automating Remote Config Sync Across Multiple Firebase Projects

Engineering

CI/CD

Firebase

Summary

This article presents an automated solution using Firebase Admin SDK, CI/CD pipelines, and GitHub Actions to manage and synchronize Firebase Remote Configs across different project environments. This approach addresses the issue of inconsistent config propagation, ensuring uniform app behavior across environments.

Key insights:
  • Remote Config Capabilities: Firebase Remote Config allows for dynamic adjustments in app behavior without an app update, supporting various parameter types and conditions based on user attributes or device characteristics.

  • Challenges in Multi-environment Management: Developers often face difficulties ensuring consistency in remote configurations across multiple environments like development, staging, and production, particularly when access is restricted to development settings.

  • Automated Synchronization Solution: To maintain uniform configurations across all environments and reduce manual errors, the implementation of CI/CD pipelines is suggested. These pipelines automate the fetching and updating of remote configs using scripts.

  • GitHub Actions for Continuous Integration: The use of GitHub Actions facilitates the continuous integration of changes across environments. The process involves creating a YAML file in the repository that triggers actions like fetching updated configurations from development and pushing them to production upon merging changes to the main branch.

  • Scripting for Config Management: A Node.js script is employed to manage the synchronization process. This script initializes Firebase projects, fetches the latest remote config template from the development project, and updates the production environment to ensure consistency.

  • Security and Efficiency: The workflow enhances security by dynamically generating necessary credential files from stored GitHub Secrets and deleting them post-execution to prevent sensitive data exposure, ensuring an efficient and secure synchronization process.

Introduction

Managing remote configurations across different Firebase projects, such as Development and Production environments, can be challenging and prone to inconsistencies. In this article, we explore how to leverage the Firebase Admin SDK to programmatically manage and synchronize remote configurations (configs) across multiple Firebase projects. This approach ensures that changes made in the development environment are seamlessly propagated to production, reducing the risk of hidden issues caused by serialization errors and configuration mismatches. This guide is particularly valuable for engineers who often only have access to the development environment and need to maintain consistency across all environments.

What is Firebase Remote Config?

Firebase Remote Config is a cloud service that allows you to change the behavior of your app without publishing an app update. It can be used for anything–from a text change to blocking users from using an app feature that might need maintenance–without requiring users to update their apps.

Firebase Remote Config allows the developers to use conditions to show a different behavior, based on the users’ attributes, app version, device characteristics, or other predefined conditions. It also supports different types of parameter types, including booleans, strings, numbers, and JSON objects, providing flexibility in configuring different aspects of your app.

The Problem

When working on a project with multiple environments such as development, staging, and production, developers often have access to only one environment, such as development. When they make changes to the remote config, they might not be able to manually update the configs in other environments. Even when they do have access, it is easy to forget to update the other environments, leading to potential inconsistencies. 

Proposed Solution

To overcome this problem and maintain consistency across environments, developers can use CI/CD pipelines. By creating scripts that automatically fetch the latest Remote Config template, typically from the development environment, and publishing it to other Firebase environments, you can ensure uniformity when deploying a new version of your app or even just merging a pull request to your main branch.

In this insight, we will demonstrate how to use GitHub Actions to help developers keep their Firebase Remote Config consistent across all their environments.

We will consider two example projects for this article: Project - Development and Project - Production

Project - Development will serve as the source with the updated template, while Project - Production will be the target environment to be updated.

The Workflow

This is what a typical workflow for our case should look like:

Make YAML File: Create a new GitHub Actions workflow YAML file, or modify an existing one, in the .github/workflows/ directory of your repository. It can be named to your liking, for example remote_config_sync.yaml.

Complete Installations: Ensure that Node.js and Firebase Admin SDK are installed in the workflow.

Run the Script: Define a job that runs your Node.js script with the appropriate command. (Example: ‘node scripts/index.js sync’. We will talk about the script in the next section.)

Here is what your YAML file should look like:

name: Firebase Remote Config Sync
on:
 push:
   branches:
     - main # Specify branch name
jobs:
 sync-remote-config:
   runs-on: ubuntu-latest
   steps:
     - name: Checkout repository
       uses: actions/checkout@v3
     - name: Set up Node.js
       uses: actions/setup-node@v3
       with:
         node-version: '20' # Specify the Node.js version
     - name: Install dependencies
       run: npm install firebase-admin
     - name: Set up credentials for PROJECT_DEVELOPMENT and PROJECT_PRODUCTION
       run: |
         echo "${{ secrets.PROJECT_DEVELOPMENT_CREDENTIALS_JSON }}" > project_dev.json
         echo "${{ secrets.PROJECT_PRODUCTION_CREDENTIALS_JSON }}" > project_prod.json
     - name: Sync Remote Config from PROJECT_DEVELOPMENT to PROJECT_PRODUCTION
       run: node scripts/index.js sync
       env:
         PROJECT_DEVELOPMENT_CREDENTIALS: ${{ github.workspace }}/project_dev.json
         PROJECT_PRODUCTION_CREDENTIALS: ${{ github.workspace }}/project_prod.json
     - name: Clean up credentials
       run: |
         rm project_dev.json
         rm project_prod.json

The PROJECT_DEVELOPMENT_CREDENTIALS and PROJECT_PRODUCTION_CREDENTIALS environment variables point to your service accounts’ JSON file, which is required for authenticating with Firebase.

Store the JSON content of your service account keys for Project - Development and Project - Production in the GitHub secrets as PROJECT_DEVELOPMENT_CREDENTIALS_JSON and PROJECT_PRODUCTION_CREDENTIALS_JSON. This way, your workflow can generate the necessary credentials files dynamically.

After the job is complete, the workflow deletes the project_dev.json and project_prod.json files to avoid leaving sensitive data on the runner.

This workflow will run every time a new branch is merged into the main branch of your repository.

The Script

After setting up your workflow, this is how to start writing the script for syncing the Firebase Remote Config templates of the two projects. 

Create a new file index.ts (can be named to your liking), in a scripts directory in your project’s repository. Then, you can start writing scripts to fetch and update templates.

1. Initializing Your Apps

Write a function to initialize a Firebase project. As we want to sync the Firebase Remote Config of multiple projects, we need to initialize them within the same script.

function initializeFirebaseWithCredentials(credentialsPath, appName) {
   return admin.initializeApp({
       credential: admin.credential.cert(require(credentialsPath)),
   }, appName);
}

2. Fetching the Updated Template

Write a function to get the Firebase Remote Config template from Project - Development and save it in a JSON file like config.json.

function getTemplate() {
   return new Promise((resolve, reject) => {
       const projectDevelopmentCredentials = process.env.PROJECT_DEVELOPMENT_CREDENTIALS;
       if (!projectDevelopmentCredentials) {
           console.error('Error: PROJECT_DEVELOPMENT_CREDENTIALS environment variable is not set.');
           process.exit(1);
       }
       const app = initializeFirebaseWithCredentials(projectDevelopmentCredentials, 'Project - Development');
       const config = app.remoteConfig();
       config.getTemplate()
           .then(template => {
               const templateStr = JSON.stringify(template);
               fs.writeFileSync('config.json', templateStr);
               resolve();
           })
           .catch(err => {
               console.error('Unable to get template');
               console.error(err);
               reject(err);
           });
   });
}

3. Updating the Other Templates

Similarly, write a function that publishes the template stored in config.json to Project - Production.

function publishTemplate() {
   return new Promise(async (resolve, reject) => {
       const projectProductionCredentials = process.env.PROJECT_PRODUCTION_CREDENTIALS;
       if (!projectProductionCredentials) {
           console.error('Error: PROJECT_PRODUCTION_CREDENTIALS environment variable is not set.');
           process.exit(1);
       }
       const app = initializeFirebaseWithCredentials(projectProductionCredentials, 'Project - Production');
       const config = app.remoteConfig();
       // Get the updated Template from the config file that came from getTemplate()
       const newTemplate = config.createTemplateFromJSON(
           fs.readFileSync('config.json', 'utf-8')
       );
       // Get the current active template.
       const currentTemplate = await config.getTemplate();
       // Merge newTemplate parameters into currentTemplate
       currentTemplate.parameters = {
           ...currentTemplate.parameters,
           ...newTemplate.parameters
       };
       config.publishTemplate(currentTemplate)
           .then(updatedTemplate => {
               console.log('Template has been published');
               resolve();
           })
           .catch(err => {
               console.error('Unable to publish template.');
               console.error(err);
               reject(err);
           });
   });
}

Notice that before updating the template on the other project we merge the parameters of newTemplate into currentTemplate. This is so that we do not run into errors like ETag mismatches.

4. Consolidating Everything

Here is what your index.ts should look like along with the syncTemplates() function.

const fs = require('fs');
const admin = require('firebase-admin');
/**
* Initialize Firebase Admin SDK with a specific credentials file.
*/
function initializeFirebaseWithCredentials(credentialsPath, appName) {
   return admin.initializeApp({
       credential: admin.credential.cert(require(credentialsPath)),
   }, appName);
}
/**
* Retrieve the current Firebase Remote Config template from the server.
* Uses the credentials specified by the `PROJECT_DEVELOPMENT_CREDENTIALS` environment variable.
*/
function getTemplate() {
   return new Promise((resolve, reject) => {
       const projectDevelopmentCredentials = process.env.PROJECT_DEVELOPMENT_CREDENTIALS;
       if (!projectDevelopmentCredentials) {
           console.error('Error: PROJECT_DEVELOPMENT_CREDENTIALS environment variable is not set.');
           process.exit(1);
       }
       const app = initializeFirebaseWithCredentials(projectDevelopmentCredentials, 'Project - Development');
       const config = app.remoteConfig();
       config.getTemplate()
           .then(template => {
               console.log('ETag from server: ' + template.etag);
               const templateStr = JSON.stringify(template);
               fs.writeFileSync('config.json', templateStr);
               resolve();
           })
           .catch(err => {
               console.error('Unable to get template');
               console.error(err);
               reject(err);
           });
   });
}
/**
* Publish the local template stored in `config.json` to the server.
* Uses the credentials specified by the `PROJECT_PRODUCTION_CREDENTIALS` environment variable.
*/
function publishTemplate() {
   return new Promise(async (resolve, reject) => {
       const projectProductionCredentials = process.env.PROJECT_PRODUCTION_CREDENTIALS;
       if (!projectProductionCredentials) {
           console.error('Error: PROJECT_PRODUCTION_CREDENTIALS environment variable is not set.');
           process.exit(1);
       }
       const app = initializeFirebaseWithCredentials(projectProductionCredentials, 'Project - Production');
       const config = app.remoteConfig();
       // Get the updated Template from the config file that came from getTemplate()
       const newTemplate = config.createTemplateFromJSON(
           fs.readFileSync('config.json', 'utf-8')
       );
       // Get the current active template.
       const currentTemplate = await config.getTemplate();
       // Merge newTemplate parameters into currentTemplate
       currentTemplate.parameters = {
           ...currentTemplate.parameters,
           ...newTemplate.parameters
       };
       config.publishTemplate(currentTemplate)
           .then(updatedTemplate => {
               console.log('Template has been published');
               console.log('ETag from server: ' + updatedTemplate.etag);
               resolve();
           })
           .catch(err => {
               console.error('Unable to publish template.');
               console.error(err);
               reject(err);
           });
   });
}
/**
* Get the template from Project - Development and publish it to Project - Production.
*/
async function syncTemplates() {
   console.log('Starting sync process...');
   await getTemplate();
   await publishTemplate();
   console.log('Sync process completed successfully.');
}
const action = process.argv[2];
if (action === 'get') {
   getTemplate();
} else if (action === 'publish') {
   publishTemplate();
} else if (action === 'sync') {
   syncTemplates();
} else {
   console.log(`
Invalid command. Please use one of the following:
node index.js get
node index.js publish
node index.js sync
   `);
}

Conclusion

To conclude, Firebase Remote Config is an excellent way to change the behavior of the app in runtime, though there is a significant problem that developers can face when having to keep up with different environments on their projects. Automating Firebase Remote Config synchronization across environments ensures consistency and reduces the risk of errors. By using CI/CD pipelines and GitHub Actions, developers can effortlessly keep their configurations aligned, leading to a more reliable and efficient deployment process.

Build Robust Applications with Expert Engineering Practices

Partner with Walturn to build high-quality applications using industry best practices, including CI/CD pipelines and seamless configuration management. Our engineering team specializes in creating robust, reliable products while maintaining consistency across all environments. Let us help you innovate and scale with confidence.

References

Aravind, Manu. “Firebase Remote Config Android - Manu Aravind - Medium.” Medium, 29 Dec. 2023, medium.com/@mobiledev4you/firebase-remote-config-android-ef24eb65d7c5

Firebase. “GitHub - Firebase/Quickstart-nodejs.” GitHub, github.com/firebase/quickstart-nodejs

“Firebase Remote Config.” Firebase, firebase.google.com/docs/remote-config

Other Insights

Got an app?

We build and deliver stunning mobile products that scale

Got an app?

We build and deliver stunning mobile products that scale

Got an app?

We build and deliver stunning mobile products that scale

Got an app?

We build and deliver stunning mobile products that scale

Got an app?

We build and deliver stunning mobile products that scale

Our mission is to harness the power of technology to make this world a better place. We provide thoughtful software solutions and consultancy that enhance growth and productivity.

The Jacx Office: 16-120

2807 Jackson Ave

Queens NY 11101, United States

Book an onsite meeting or request a services?

© Walturn LLC • All Rights Reserved 2024

Our mission is to harness the power of technology to make this world a better place. We provide thoughtful software solutions and consultancy that enhance growth and productivity.

The Jacx Office: 16-120

2807 Jackson Ave

Queens NY 11101, United States

Book an onsite meeting or request a services?

© Walturn LLC • All Rights Reserved 2024

Our mission is to harness the power of technology to make this world a better place. We provide thoughtful software solutions and consultancy that enhance growth and productivity.

The Jacx Office: 16-120

2807 Jackson Ave

Queens NY 11101, United States

Book an onsite meeting or request a services?

© Walturn LLC • All Rights Reserved 2024

Our mission is to harness the power of technology to make this world a better place. We provide thoughtful software solutions and consultancy that enhance growth and productivity.

The Jacx Office: 16-120

2807 Jackson Ave

Queens NY 11101, United States

Book an onsite meeting or request a services?

© Walturn LLC • All Rights Reserved 2024

Our mission is to harness the power of technology to make this world a better place. We provide thoughtful software solutions and consultancy that enhance growth and productivity.

The Jacx Office: 16-120

2807 Jackson Ave

Queens NY 11101, United States

Book an onsite meeting or request a services?

© Walturn LLC • All Rights Reserved 2024