[React] Passing Environment Variables to Service Workers
Tags: javascript, react, node, firebase
Published: 2024-01-18 04:33:56

When working with [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging/js/receive) in a React application, creating a service worker (the following `firebase-messaging-sw.js`) in the public directory becomes essential. However, handling environment-specific configurations, such as API keys, poses a challenge due to the inability to access environment variables using process.env within the public directory. This blog post explores two approaches to dynamically pass environment variables to the service worker.

```javascript
// public/firebase-messaging-sw.js

// Give the service worker access to Firebase Messaging.
// Note that you can only use Firebase Messaging here. Other Firebase libraries are not available in the service worker.
importScripts('https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/8.10.1/firebase-messaging.js');

// Initialize the Firebase app in the service worker by passing in your app's Firebase config object.
// https://firebase.google.com/docs/web/setup#config-object
firebase.initializeApp({
  apiKey: 'api-key',
  authDomain: 'project-id.firebaseapp.com',
  databaseURL: 'https://project-id.firebaseio.com',
  projectId: 'project-id',
  storageBucket: 'project-id.appspot.com',
  messagingSenderId: 'sender-id',
  appId: 'app-id',
  measurementId: 'G-measurement-id',
});

// Retrieve an instance of Firebase Messaging so that it can handle background messages.
const messaging = firebase.messaging();
```

## Using a Separate Script to Build Environment Variables

Create a `.env` file with your Firebase configurations.

```yml
REACT_APP_FIREBASE_API_KEY = 'api-key'
REACT_APP_FIREBASE_AUTH_DOMAIN = 'project-id.firebaseapp.com',
REACT_APP_FIREBASE_PROJECT_ID = 'project-id'
REACT_APP_FIREBASE_STORAGE_BUCKET = 'project-id.appspot.com'
REACT_APP_FIREBASE_MESSAGING_SENDER_ID = 'sender-id'
REACT_APP_FIREBASE_APP_ID = 'app-id'
REACT_APP_FIREBASE_MEASUREMENT_ID = 'G-measurement-id'
```

Create a script (`swEnvBuild.js`) to build a JavaScript file (`swEnv.js`) containing the environment variables.

```javascript
// src/swEnvBuild.js

const fs = require("fs");

// This dotenv is the one used inside React.
// Load dotenv Intentionally because build process does not have access to .env file yet.
const dotenv = require("dotenv");
dotenv.config();

const {
  REACT_APP_FIREBASE_API_KEY,
  REACT_APP_FIREBASE_AUTH_DOMAIN,
  REACT_APP_FIREBASE_PROJECT_ID,
  REACT_APP_FIREBASE_STORAGE_BUCKET,
  REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
  REACT_APP_FIREBASE_APP_ID,
  REACT_APP_FIREBASE_MEASUREMENT_ID,
} = process.env;

const content = `const swEnv = {
    REACT_APP_FIREBASE_API_KEY: '${REACT_APP_FIREBASE_API_KEY}',
    REACT_APP_FIREBASE_AUTH_DOMAIN: '${REACT_APP_FIREBASE_AUTH_DOMAIN}',
    REACT_APP_FIREBASE_PROJECT_ID: '${REACT_APP_FIREBASE_PROJECT_ID}',
    REACT_APP_FIREBASE_STORAGE_BUCKET: '${REACT_APP_FIREBASE_STORAGE_BUCKET}',
    REACT_APP_FIREBASE_MESSAGING_SENDER_ID: '${REACT_APP_FIREBASE_MESSAGING_SENDER_ID}',
    REACT_APP_FIREBASE_APP_ID: '${REACT_APP_FIREBASE_APP_ID}',
    REACT_APP_FIREBASE_MEASUREMENT_ID: '${REACT_APP_FIREBASE_MEASUREMENT_ID}'
 }`;

fs.writeFileSync("./public/swEnv.js", content);
```

Modify the `npm start` command in `package.json` to run the build script before starting the development server. Only the `npm start` command is being modified for testing in the development environment. When deploying, I think it is necessary to modify the `npm build` command.

```diff
"scripts": {
- "start": "react-scripts start",
+ "start": "node src/swEnvBuild.js && react-scripts start",
```

Now, running `npm run start` will create `public/swEnv.js`.

```javascript
// public/swEnv.js

const swEnv = {
  REACT_APP_FIREBASE_API_KEY: 'api-key',
  REACT_APP_FIREBASE_AUTH_DOMAIN: 'project-id.firebaseapp.com',
  REACT_APP_FIREBASE_PROJECT_ID: 'project-id',
  REACT_APP_FIREBASE_STORAGE_BUCKET: 'project-id.appspot.com',
  REACT_APP_FIREBASE_MESSAGING_SENDER_ID: 'sender-id',
  REACT_APP_FIREBASE_APP_ID: 'app-id',
  REACT_APP_FIREBASE_MEASUREMENT_ID: 'G-measurement-id'
}
```

Update `public/firebase-messaging-sw.js` to import and use the dynamically generated environment variables.

```javascript
// public/firebase-messaging-sw.js

importScripts("https://www.gstatic.com/firebasejs/8.3.1/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/8.3.1/firebase-messaging.js");
importScripts("swEnv.js"); // Added

firebase.initializeApp({
  apiKey: swEnv.REACT_APP_FIREBASE_API_KEY, // Changed... repeat for other variables
  authDomain: swEnv.REACT_APP_FIREBASE_AUTH_DOMAIN,
  projectId: swEnv.REACT_APP_FIREBASE_PROJECT_ID,
  storageBucket: swEnv.REACT_APP_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: swEnv.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
  appId: swEnv.REACT_APP_FIREBASE_APP_ID,
  measurementId: swEnv.REACT_APP_FIREBASE_MEASUREMENT_ID,
});

const messaging = firebase.messaging();
```


## Using Query Parameters

Call `registerServiceWorker` in `src/App.jsx` to dynamically construct the service worker script URL with query parameters.

```javascript
export const registerServiceWorker = async () => {
  let scriptURL = "firebase-messaging-sw.js";
  scriptURL += `?apiKey=${process.env.REACT_APP_FIREBASE_API_KEY}`;
  scriptURL += `&authDomain=${process.env.REACT_APP_FIREBASE_AUTH_DOMAIN}`;
  scriptURL += `&projectId=${process.env.REACT_APP_FIREBASE_PROJECT_ID}`;
  scriptURL += `&storageBucket=${process.env.REACT_APP_FIREBASE_STORAGE_BUCKET}`;
  scriptURL += `&messagingSenderId=${process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID}`;
  scriptURL += `&appId=${process.env.REACT_APP_FIREBASE_APP_ID}`;
  scriptURL += `&measurementId=${process.env.REACT_APP_FIREBASE_MEASUREMENT_ID}`;

  if ("serviceWorker" in navigator) {
    try {
      const registration = await navigator.serviceWorker.register(scriptURL);
      if (registration.installing) {
        console.log("Service worker installing");
      } else if (registration.waiting) {
        console.log("Service worker installed");
      } else if (registration.active) {
        console.log("Service worker active");
      }
    } catch (error) {
      console.error(`Registration failed with ${error}`);
    }
  }
};
```

Update `public/firebase-messaging-sw.js` to extract query parameters and initialize Firebase.

```javascript
// public/firebase-messaging-sw.js

importScripts("https://www.gstatic.com/firebasejs/8.3.1/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/8.3.1/firebase-messaging.js");

const params = new URL(location).searchParams;
const apiKey = params.get("apiKey");
const authDomain = params.get("authDomain");
const projectId = params.get("projectId");
const storageBucket = params.get("storageBucket");
const messagingSenderId = params.get("messagingSenderId");
const appId = params.get("appId");
const measurementId = params.get("measurementId");

firebase.initializeApp({
  apiKey: apiKey,
  authDomain: authDomain,
  projectId: projectId,
  storageBucket: storageBucket,
  messagingSenderId: messagingSenderId,
  appId: appId,
  measurementId: measurementId,
});

const messaging = firebase.messaging();
```

Both approaches provide solutions for passing environment variables to service workers in a React application. Choose the approach that best fits your project requirements and development workflow. Additionally, alternative methods, such as using Webpack and Babel or [workbox-webpack-plugin](https://developer.chrome.com/docs/workbox/modules/workbox-webpack-plugin) with InjectManifest are available. I will try these as well when I get a chance.

That is about it. Happy coding!