How to tell a Vue app to use Firebase emulator?

UPDATED on 14 Jan 2021 to reflect changes to Firebase SDK.

Official documentation on connecting to Firestore emulator is here: https://firebase.google.com/docs/emulator-suite/connect_firestore

Official documentation on connecting to Functions emulator is here: https://firebase.google.com/docs/emulator-suite/connect_functions

In practice the setup will look something like this:

import * as Firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';
import 'firebase/functions';
import 'firebase/storage';

const firebaseConfig = { <Per Firebase Console> };

!Firebase.apps.length ? Firebase.initializeApp(firebaseConfig) : '';

if(window.location.hostname === 'localhost') {
  Firestore.firestore().useEmulator('localhost', 8080);
  Firestore.functions().useEmulator('localhost', 5001);
  /* OLD implementation */
  // Firebase.firestore().settings({ host: 'localhost:8080', ssl: false });
  // Firebase.functions().useFunctionsEmulator('http://localhost:5001');
}

export const GoogleAuthProvider = new Firebase.auth.GoogleAuthProvider();
export const FirebaseAuth = Firebase.auth();
export const Firestore = Firebase.firestore();
export const FirebaseFunctions = Firebase.functions();
export const FirebaseStorage = Firebase.storage();
export default Firebase;

This can be imported into Vuex store, or any other page like this:

import { Firestore, FirebaseFunctions } from '@/services/firebase.js';

Then in command prompt / terminal run:

firebase emulators:start

This will also work with Nuxt.


Ok, so this is going to be a long answer, but I'm hoping to answer your question as completely as possible. The process really works in two stages: making the emulator (including hot reloading) work with Vue, then making Vue work with the emulated version of Firebase.


Step 1: Making the Firebase emulator work with Vue

For the first step, you need to edit your package.json to setup Vue to do a watch/build cycle instead of a hot reload cycle, as shown below. The only thing that won't work (AFAIK) is the Vue DevTools extention. (Quick note, the run-s and run-p commands I'm using are from the npm-run-all package because I'm on Windows and cmd.exe doesn't like single ampersands &). Also, to use the firebase command from your scripts, you need to install the firebase-tools package as a dev dependency.

"scripts": {
  "build": "vue-cli-service build",
  "build:dev": "vue-cli-service build --mode development",
  "build:watch": "vue-cli-service build --mode development --watch --no-clean",
  "lint": "vue-cli-service lint",
  "serve": "run-s build:dev watch",
  "serve:firebase": "firebase serve",
  "watch": "run-p build:watch serve:firebase"
}

to install the dev dependencies required

  npm i --save-dev firebase-tools npm-run-all

So, that's a lot. Let me breakdown what each command is doing:

  • watch: This command is just the shell command that gets everything going. It runs the build and serve commands in series. More on this later
  • serve: This command starts the actual watch/build cycle. It starts the Firebase emulator and starts the vue-cli-service to watch for changes.
  • serve:firebase: This command starts the Firebase emulator...that's all.
  • build: This command does a production build...not really used here, but it's left in for completeness.
  • build:dev: This command is a bit important. If you notice, in the initial watch script, we called the build:dev script first. This is because if you start the Firebase emulator and your "public" directory (note: this is Firebase's public directory, not Vue's) gets deleted, then Firebase will actually crash. So, to counteract this, we complete a build before starting the build/watch cycle.
  • build:watch: This is where the hot reload magic happens. We tell the vue-cli-service to build the app, but also to watch for changes and not clean the build directory. The watching for changes starts the watch/build cycle mentioned earlier. We tell it to not clean the build directory because Firebase doesn't care if the files inside the directory its serving from change, but it will crash if the directory gets deleted.

The only downside to this method is that the Vue DevTools do not work.


Step 2: Making Vue work with the Firebase emulator

Turns out there is actually a very simple solution to this problem thanks to the Firebase Documentation. What you need to do is request a special file from the Firebase reserved URLs. In my example, I use Axios, but by all means, feel free to use whatever library for making requests you would like.

import axios from 'axios';
import firebase from '@firebase/app';
import '@firebase/auth';
import '@firebase/firestore';
import '@firebase/functions';

axios.get('/__/firebase/init.json').then(async response => {
  firebase.initializeApp(await response.data);
});

export default firebase;

Also, to add a property to the Vue instance, it would be better to do this, to avoid any issues with garbage collection or naming collisions. Then, within any Vue component, you can just use this.$firebase.

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import firebase from './plugins/firebase';

Vue.config.productionTip = false;
Vue.prototype.$firebase = firebase;

new Vue({
  router,
  store,
  render: h => h(App),
}).$mount('#app');

Ideally, there would be some way to distinguish whether the application is running within the emulator or not, but in practice the only problem it would solve is the ability to use the Vue DevTools extension, which I don't really view (pun intended) as a requirement. But, with all of that out of the way, you should be up and running in the emulator, with live reloading; and, to top it all off, once your ready, you don't have to make any changes to your application to deploy it.


Bonus: Deployment

So, here is another scripts section that has all of the same things as above, but also includes a 1-command deploy to make sure you deploy a production build from Vue to Firebase.

"scripts": {
  "build": "vue-cli-service build",
  "build:dev": "vue-cli-service build --mode development",
  "build:watch": "vue-cli-service build --mode development --watch --no-clean",
  "deploy": "run-s build deploy:firebase",
  "deploy:firebase": "firebase deploy",
  "lint": "vue-cli-service lint",
  "serve": "run-s build:dev watch",
  "serve:firebase": "firebase serve",
  "watch": "run-p build:watch serve:firebase"
}

Updated with firebase web version 9 in Nov 2021: Using the same configuration setup as starleaf1 change the firebase.js to the following:

import { initializeApp } from "firebase/app";
import { getAuth, connectAuthEmulator } from "firebase/auth";
import { getFirestore, connectFirestoreEmulator } from "firebase/firestore";

initializeApp({
    apiKey: "xxx",
    authDomain: "xxx",
    projectId: "xxx",
    storageBucket: "xxx",
    messagingSenderId: "xxx",
    appId: "xxx"
});

const db = getFirestore();
const auth = getAuth();

// If on localhost, use all firebase services locally
if (location.hostname === 'localhost') {
    connectFirestoreEmulator(db, 'localhost', 8080);
    connectAuthEmulator(auth, "http://localhost:9099");
    // add more services as described in the docs: https://firebase.google.com/docs/emulator-suite/connect_firestore
}

export { db, auth };

That's it :-) It took me over 10 h to figure this out.