Firebase Functions
Learn how to integrate Doppler with Firebase Cloud Functions to sync secrets to production and during local development.
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
- Doppler Project created
- Doppler CLI installed
- Firebase application deployed
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:
- Update the function environment variables
- 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:
- Downgrade to
[email protected]
until they decide to make changes (please add your feedback to this GitHub issue!). - 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.
Updated almost 2 years ago