Developer Blog

Building a Chrome Extension Using Firebase Real-Time Database

Recently, I was tasked to prototype a solution that was most easily solved by building a native Chrome Extension alongside a companion web application. I needed a way for these two applications to share data amongst each other, and from a bit of research I learned that Chrome Extensions have an API for interacting with Chrome Storage, but interacting with the browser local storage was cumbersome. To bridge the data-gap between these two applications, I brought in Firebase Real-Time Database (RTD). We have been using it for several projects in the Originate portfolio and it has proven to be a very valuable tool. Firebase is essentially a backend-as-a-service that hosts your data and allows multiple applications to hook into it. While both were interesting, in this post, I will walk you through how to integrate Firebase RTD into a Chrome App.

Configuring Firebase for Chrome Extensions

If this is your first time creating a Chrome Extension, I recommend this awesome boilerplate. Hooking up Firebase to your Chrome Extension requires an added layer of complexity due to certain restraints on security as well as architecture best practices. Chrome Apps have a specific Content Security Policy (CSP) which prevents inline scripting and instantiates that policy to mitigate against cross-site scripting issues. There are more compliancy rules to consider generally, but for the purpose of Firebase configuration, you will need to specify that certain Google/Firebase routes are in the clear. To configure your Chrome App, a great place to start is the Authentication Boilerplate as a guideline for the following two preliminary steps:

  • import the Firebase library as an inline script in your HTML template.
  • update the content_security_policy value in your manifest.json file with the following:
1
2
3
4
5
{
  ...
  "content_security_policy": "script-src 'self' https://www.gstatic.com/ https://*.firebaseio.com https://www.googleapis.com; object-src 'self'; connect-src 'self' wss://*.firebaseio.com;",
  ...
}

These rules let your Chrome Extension download the Firebase client library and connect to Firebase’s websocket server.

Note that the Firebase Authentication boilerplate has the configuration for protecting your Extension with a login screen. It is a great reference if you are building more than a rapid prototype and are looking into protecting your database with proper ruling.

As a result of the way the library needs to be imported, your Firebase app (as well as the RTD event hooks) will need to be instantiated in the background script. Data changes will alter state housed in the Chrome Storage which is accessible to your background script, the popup render code, and in content scripts. Each operates within its own scope as background scripts run on Extension load, content scripts are JavaScript files that run in the context of web pages, and popup render code runs when the menu is opened. In order to access your Firebase database, you will need to utilize the Chrome Message API to relay changes to the RTD.

Execution

What does this look like in practice? Let’s dig into some code:

background.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// this file will run once on extension load
var config = {
  apiKey: "[insert api key]",
  authDomain: "[insert auth domain]",
  databaseURL: "[insert database url]",
  projectId: "[insert project id]",
  storageBucket: "[insert storage bucket]",
  messagingSenderId: "[insert message sender id]"
};
const app = firebase.initializeApp(config);
const appDb = app.database().ref();


// instantiate global application state object for Chrome Storage and feed in firebase data
// Chrome Storage will store our global state as a a JSON stringified value.

const applicationState = { values: [] };

appDb.on('child_added', snapshot => {
  applicationState.values.push({
    id: snapshot.key,
    value: snapshot.val()
  });
  updateState(applicationState);
});

appDb.on('child_removed', snapshot => {
  const childPosition = getChildIndex(applicationState, snapshot.key)
  if (childPosition === -1) return
  applicationState.values.splice(childPosition, 1);
  updateState(applicationState);
});

appDb.on('child_changed', snapshot => {
  const childPosition = getChildIndex(applicationState, snapshot.key)
  if (childPosition === -1) return
  applicationState.values[childPosition] = snapshot.val();
  updateState(applicationState);
});

// updateState is a function that writes the changes to Chrome Storage
function updateState(applicationState) {
  chrome.storage.local.set({ state: JSON.stringify(applicationState) });
}

// getChildIndex will return the matching element in the object
function getChildIndex(appState, id) {
  return appState.values.findIndex(element => element.id == id)
}

// if your Chrome Extension requires any content scripts that will manipulate data,
// add a message listener here to access appDb:

chrome.runtime.onMessage.addListener((msg, sender, response) => {
  switch (msg.type) {
    case 'updateValue':
      appDb.child(msg.opts.id).set({ value: msg.opts.value });
      response('success');
      break;
    default:
      response('unknown request');
      break;
  }
});

That’s it! All your extension needs to do is load from the Chrome local storage before rendering itself. If it or a content script needs to manipulate the RTD, the following snippet should be invoked:

1
2
3
4
5
chrome.runtime.sendMessage({type: 'updateValue', opts: request.opts}, (response) => {
  if(response == 'success') {
    // implement success action here              
  }
});

By following these steps, you should now have a Chrome Extension that will stay in sync with other companion applications! Now go celebrate the fruits of your labor. Happy hacking!

Comments