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
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'
- 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
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();
const newTemplate = config.createTemplateFromJSON(
fs.readFileSync('config.json', 'utf-8')
);
const currentTemplate = await config.getTemplate();
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');
function initializeFirebaseWithCredentials(credentialsPath, appName) {
return admin.initializeApp({
credential: admin.credential.cert(require(credentialsPath)),
}, appName);
}
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);
});
});
}
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();
const newTemplate = config.createTemplateFromJSON(
fs.readFileSync('config.json', 'utf-8')
);
const currentTemplate = await config.getTemplate();
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);
});
});
}
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.