Securing Firebase Configuration in Production: A Best Practice Guide

Securing Firebase Configuration in Production: A Best Practice Guide

When building web applications with Firebase, managing your Firebase configuration securely is essential. While Firebase API keys themselves aren’t sensitive secrets (as Firebase uses security rules for access control), it’s still a best practice to avoid hardcoding configuration in your production code.

In this guide, I’ll share a robust approach to handling Firebase configuration that:

  1. Uses Firebase Hosting’s auto-configuration in production
  2. Falls back to local configuration during development
  3. Ensures configuration values aren’t bundled into production builds

The Challenge

Many Firebase applications start with a configuration block like this:

const firebaseConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "your-app.firebaseapp.com",
  projectId: "your-app",
  storageBucket: "your-app.appspot.com",
  messagingSenderId: "123456789",
  appId: "1:123456789:web:abcdef",
  measurementId: "G-ABCDEF123"
};

firebase.initializeApp(firebaseConfig);

While this works, it has several drawbacks:

  • Configuration is hardcoded in your source code
  • Values get bundled into your production JavaScript
  • Changes require code modifications and redeployment

The Solution: Firebase Hosting Auto-Configuration

Firebase Hosting provides a built-in feature to inject your Firebase configuration at runtime. This approach:

  1. Keeps configuration out of your source code
  2. Automatically updates when your Firebase project changes
  3. Works seamlessly with Firebase Hosting

Here’s how to implement it:

Step 1: Update HTML Files to Load Firebase Scripts

Replace your standard Firebase script imports with special paths that Firebase Hosting recognizes:

<!-- Firebase App (the core Firebase SDK) -->
<script>
  // Determine if we're in development or production
  const isDev = window.location.hostname === 'localhost' || 
                window.location.hostname.includes('127.0.0.1');

  // Create script elements with the appropriate src based on environment
  const scripts = [
    isDev ? 'https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js' : '/__/firebase/8.10.1/firebase-app.js',
    isDev ? 'https://www.gstatic.com/firebasejs/8.10.1/firebase-auth.js' : '/__/firebase/8.10.1/firebase-auth.js',
    isDev ? 'https://www.gstatic.com/firebasejs/8.10.1/firebase-firestore.js' : '/__/firebase/8.10.1/firebase-firestore.js',
    isDev ? 'https://www.gstatic.com/firebasejs/8.10.1/firebase-analytics.js' : '/__/firebase/8.10.1/firebase-analytics.js'
  ];

  // Add initialization script for production
  if (!isDev) {
    scripts.push('/__/firebase/init.js');
  }

  // Load scripts in sequence
  scripts.forEach(src => {
    const script = document.createElement('script');
    script.src = src;
    script.defer = true;
    document.head.appendChild(script);
  });
</script>

This code dynamically loads Firebase scripts from:

  • Standard CDN URLs during local development
  • Special /__/firebase/ paths in production, which Firebase Hosting serves with your configuration

Step 2: Configure Vite to Handle Environment-Specific Code

If you’re using Vite (or similar bundlers), configure it to handle environment-specific code:

// vite.config.js
export default defineConfig(({ mode }) => {
  // Determine if we're building for production
  const isProduction = mode === 'production';

  console.log(`Building for ${isProduction ? 'production' : 'development'} mode`);

  return {
    // Define environment variables to be replaced at build time
    define: {
      // This will replace all instances of process.env.FIREBASE_CONFIG_ENABLED with 'false' in production
      // and 'true' in development, allowing us to conditionally import the config
      'process.env.FIREBASE_CONFIG_ENABLED': isProduction ? 'false' : 'true',
      // Also define NODE_ENV for consistency
      'process.env.NODE_ENV': JSON.stringify(mode),
    },
    // ... other configuration
  };
});

Step 3: Create a Development-Only Configuration File

Create a separate file for development configuration:

// firebase-config.js - ONLY used in development
const firebaseConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "your-app.firebaseapp.com",
  projectId: "your-app",
  // ... other configuration values
};

export default firebaseConfig;

Step 4: Update Firebase Initialization to Handle Both Environments

// firebase-init.js
import logger from './utils/logger';

// Conditionally import Firebase configuration for development mode
// This will be tree-shaken out in production builds
let devFirebaseConfig = null;
if (process.env.FIREBASE_CONFIG_ENABLED === 'true') {
  // Dynamic import to ensure it's not included in production builds
  import('./firebase-config').then(module => {
    devFirebaseConfig = module.default;
  });
}

// Determine if we're in development or production
const isDevelopment = window.location.hostname === 'localhost' || 
                      window.location.hostname.includes('127.0.0.1');

// Function to initialize Firebase
export async function initializeFirebaseAndWait() {
  try {
    // Check if Firebase SDK is loaded
    if (typeof firebase === 'undefined') {
      console.error('Firebase SDK not loaded');
      return;
    }

    if (isDevelopment) {
      // In development, initialize Firebase with local config
      console.log('Initializing Firebase with development configuration...');

      // If we haven't loaded the config yet, load it dynamically
      if (devFirebaseConfig === null && process.env.FIREBASE_CONFIG_ENABLED === 'true') {
        try {
          const module = await import('./firebase-config');
          devFirebaseConfig = module.default;
        } catch (error) {
          console.error('Failed to load Firebase configuration:', error);
          return;
        }
      }

      if (firebase.apps.length === 0) {
        firebase.initializeApp(devFirebaseConfig);
      }
    } else {
      // In production, Firebase is already initialized by the auto-configuration script
      console.log('Using Firebase auto-configuration from Firebase Hosting');
    }

    // Firebase is now initialized
    return firebase;
  } catch (error) {
    console.error('Failed to initialize Firebase:', error);
    throw error;
  }
}

How It Works

This approach creates a seamless experience across environments:

  1. In Development:
  • Loads Firebase scripts from the public CDN
  • Dynamically imports your local configuration file
  • Manually initializes Firebase with your configuration
  1. In Production:
  • Loads Firebase scripts from Firebase Hosting’s special paths
  • Uses the /__/firebase/init.js script to automatically initialize Firebase
  • Your configuration file is never bundled into production code

Verifying the Configuration is Not Bundled

To verify that your Firebase configuration isn’t included in production builds, you can:

  1. Build your project with production mode: NODE_ENV=production npm run build
  2. Search the output files for sensitive strings like “apiKey”:
   grep -r "apiKey" dist/

If implemented correctly, you should find no references to your Firebase configuration values in the production build.

Benefits of This Approach

  1. Security: Configuration values aren’t hardcoded in production JavaScript
  2. Maintainability: Configuration changes don’t require code changes
  3. Flexibility: Works seamlessly in both development and production
  4. Performance: Uses Firebase Hosting’s optimized CDN in production

Conclusion

By leveraging Firebase Hosting’s auto-configuration feature and implementing environment-specific code loading, you can create a more secure and maintainable Firebase application. This approach follows best practices for configuration management while maintaining a smooth developer experience.

Remember, while Firebase API keys themselves aren’t highly sensitive secrets (as Firebase uses security rules for access control), following these practices helps maintain a clean separation between configuration and code, making your application more maintainable and secure.

Leave a Reply