The example repository is located at https://github.com/1919yuan/nuxt-firebase-example

I’ve been volunteering in an NPO and learning webapp development recently. After procrastinating for a long time, I finally settled down on using Nuxt.js for developing the website for the NPO. Even though there are many resources on nuxtjs.org about deploying to different environments, there is not an official guide about deploying an universal mode Nuxt.js application to Firebase. This becomes more difficult when you want to leverage Firebase’s authentication and firestore service in universal mode. After a series of trial and error, and with the help of many valuable articles and repositories 123456, I was able to finally configure the app with authentication and firestore on Firebase hosting and Firebase functions. It was an interesting learning process, so I would like to share the steps to recreate a minimal working Nuxt.js app with Firebase authentication, firestore deployed to Firebase Hosting and Functions in universal mode.

Project setup

TL;DR: Create Nuxt.js repo and Firebase project.

Create a Nuxt.js project

Install nvm and use Node.js 10 runtime if you haven’t set that up already. Then create a Nuxt.js app.

npx create-nuxt-app nuxt-firebase-example

You are free to choose any options for the questions asked by the npx CLI, but below are the choices I used in the example Github repository. Be sure to choose SSR mode for your app because that’s the most difficult part that this post is about.

? Choose programming language JavaScript
? Choose the package manager Yarn
? Choose UI framework Buefy
? Choose custom server framework Express
? Choose Nuxt.js modules Axios, DotEnv
? Choose linting tools ESLint
? Choose test framework None
? Choose rendering mode Universal (SSR)

Note that after creation, nuxt-create-app adopts the default folder structure like the following:

$ ls nuxt-firebase-example
README.md  assets  components  layouts  middleware  node_modules  nuxt.config.js
package.json  pages  plugins  server  static  store  yarn.lock

However, because we want to deploy server side to Firebase functions, and client side to Firebase hosting, we need to create two packages within our project folder. Therefore, following the example in 1, we want to move the source folders down to the src/ folder, with a structure like this:

$ ls nuxt-firebase-example
README.md src node_modules nuxt.config.js package.json yarn.lock
$ ls nuxt-firebase-example/src
assets  components  layouts  middleware  pages  plugins  server  static  store

There will be a functions/ directory in parallel with the src/ folder, for deploying the built universal mode app after the next step.

Initialize the Firebase project

Then install Firebase CLI:

yarn global add firebase-tools
firebase login
# Follow the browser prompts to finish signing in your Firebase account for CLI.
firebase init

Choose “Hosting” and functions on the prompt like this:

? Which Firebase CLI features do you want to set up for this folder? Press Space
to select features, then Enter to confirm your choices.
◯ Database: Deploy Firebase Realtime Database Rules
◯ Firestore: Deploy rules and create indexes for Firestore
◉ Functions: Configure and deploy Cloud Functions
◉ Hosting: Configure and deploy Firebase Hosting sites
◯ Storage: Deploy Cloud Storage security rules
◯ Emulators: Set up local emulators for Firebase features

Create a new project using the Firebase CLI. You can do this at https://console.firebase.google.com too. Below are the options I used for the example repository.

? Please select an option: Create a new project
i  If you want to create a project in a Google Cloud organization or folder,
please use "firebase projects:create" instead, and return to this command when
you've created the project.
? Please specify a unique project id (warning: cannot be modified afterward)
[6-30 characters]:
# This project id needs to be globally unique.
() nuxt-firebase-example-1919yuan
? What language would you like to use to write Cloud Functions? JavaScript
? Do you want to use ESLint to catch probable bugs and enforce style? No
? Do you want to install dependencies with npm now? Yes
? What do you want to use as your public directory? public
? Configure as a single-page app (rewrite all urls to /index.html)? No

You would also want to create a Firebase app inside your newly created firebase project. To do this, you can either create an app at console.firebase.google.com, or use the Firebase CLI like:

$ firebase apps:create web
Create your WEB app in project nuxt-firebase-example-1919yuan:
? What would you like to call your app? nuxt
✔ Creating your Web app

🎉🎉🎉 Your Firebase WEB App is ready! 🎉🎉🎉

App information:
  - App ID: {your_app_id}
  - Display name: nuxt

After the above commands, you’ll have a root project directory like:

$ ls nuxt-firebase-example
README.md  firebase.json  functions  node_modules  nuxt.config.js  package.json
public  src  yarn.lock
$ ls nuxt-firebase-example/functions
index.js  node_modules  package.json

Install Nuxt.js modules

Now we will install related Firebase modules in addition to the initial Nuxt.js project setup.

# For the convenience of setting up Firebase client and using $fireStore
# objects across app and store.
$ yarn add @nuxtjs/firebase firebase
# Firebase Admin SDK and jwt-decode, for server side token verification.
$ yarn add firebase-admin jwt-decode
# Add corejs which is compatible with @nuxtjs/firebase
$ yarn add core-js@2.6.10
# Add cookie handling packages
$ yarn add cookie cookie-universal-nuxt

Make code and configuration changes

Setup Nuxt Firebase Module

Following the instructions by lupas@, we setup the module config for @nuxtjs/firebase:

...
modules {
...
  '@nuxtjs/firebase'
...
},
firebase: {
  config: {
    apiKey: process.env.FIREBASE_API_KEY,
    authDomain: process.env.FIREBASE_AUTH_DOMAIN,
    databaseURL: process.env.FIREBASE_DATABASE_URL,
    projectId: process.env.FIREBASE_PROJECT_ID,
    storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
    messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
    appId: process.env.FIREBASE_APP_ID,
    measurementId: process.env.FIREBASE_MEASUREMENT_ID,
  },
  services: {
    auth: true,
    firestore: true,
  }
},
build: {
  ...
  transpile: [
    ...
    '/^nuxt-fire/',
    ...
  ]
  ...
},
srcDir: 'src'

Please note here that I couldn’t really get @nuxtjs/firebase to work in universal mode by following lupas@’s guide on 2, so I am only using @nuxtjs/firebase as a convenient wrapper for the Firebase client library. I was only able to make Firebase authentication work in universal mode by following Austin’s tutorial 1. Finally we also want to modify the nuxt.config.js file located at the root of the project directory to know that the source files of our Nuxt.js project is located at the src dir.

After the above setup, you may have noticed that we have already relied on various environment variables so far:

We will set these up using our dotenv dependency chosen at the time we ran create-nuxt-app.

First, add dotenv initialization to nuxt.config.js at the top:

require('dotenv').config()

Second, create the .env file at the project root:

FIREBASE_API_KEY={your_api_key}
FIREBASE_AUTH_DOMAIN={your_project_id}.firebaseapp.com
FIREBASE_DATABASE_URL=https://{your_project_id}.firebaseio.com
FIREBASE_PROJECT_ID={your_project_id}
FIREBASE_STORAGE_BUCKET={your_project_id}.appspot.com
FIREBASE_MESSAGING_SENDER_ID={your_sender_id}
FIREBASE_APP_ID={your_app_id}
FIREBASE_MESUREMENT_ID={your_measurement_id}
GOOGLE_APPLICATION_CREDENTIALS="firebase_key.json"

The firebase_key.json file is not needed for Firebase client initialization, but Firebase Admin initialization on server side. You can download this service account key file from “console.firebase.google.com > Project Settings > Service Accounts > Generate new private key”. Save it to the root of your project dir with the name firebase_key.json.

Firebase Authentication in Universal Mode

Now we are going to follow the Austin’s article 1 to setup a Axios interceptor on client side and a Firebase admin token parser on server side.

There are many steps and Austin did a good job of explaining it. Please follow the tutorial there. The files that you’ll need to create/modify are:

src/plugins/auth-listener.js src/plugins/axios.js src/store/index.js

And of course nuxt.config.js. Please check out the example repository created for this article and get the code artifacts there.

There are two notes that I’d like to add about Austin’s article 1:

  1. The use of the __session cookie. It turns out that when I try to host my server side app on Firebase functions and client side app on Firebase hosting, the only cookie that’s able to appear on both server and client side is the __session cookie. I’ve tried to user other cookie names with lang, __lang, for my i18n work of keeping the user’s choice of language, but they have all failed to appear with the request sent to the server end on Firebase functions. (I should have read Austin’s article more carefully…) So in my example repo I changed the vuex store to use JSON encoding and decoding to handle the __session cookie.
  2. How to initialize firebase-admin. This step becomes really tricky in the SSR setup. Firebase admin must be initialized inside nuxt.config.js, before the config blob, and dotenv needs to be initialized before firebase-admin to provide access to environment variables. This same initialization needs to be done in both the root nuxt.config.js and the functions/index.js file. I also tried doing the initialization in src/store/index.js or src/server/index.js and neither works. In the failure mode, your SSR app would return the famous error message:
    ERROR  Your API key is invalid, please check you have copied it correctly.
    

Firebase functions

Now we’ve finished most of the coding for our Nuxt.js app, but we need to give it another entry point for Firebase functions. There have been many articles on how to deploy Nuxt.js’s server side app onto Firebase functions, but I was mainly stuck at two points:

  1. How to make the built artifact inside .nuxt available for the functions/ directory. There has been example Github repositories using different ways of copying .nuxt, either by specifying a buildDir field in nuxt.config.js, or by copying them explicitly before deploying to Firebase. After trial and error, I agree with 4 that the buildDir specification doesn’t work quite well: there may be dependencies that won’t work (I suspect @nuxtjs/firebase is one of them) if the .nuxt directory is not at the same level as the package.json file.
  2. How to make environment variables available for server side and client side apps. This also took me a long time to figure out: process.env.* environment variables are only available to server side app, and only those specially exposed variables in the env field of nuxt.config.js is available on client side. This is a good feature for security concerns, but it just took me too long to figure out. Another issue that took me a long time to figure out was that the dotenv config needs to be initialized both inside index.js in functions/ and nuxt.config.js. I only initialized it in src/server before, and it took me a long time to debug and realize that my environment variables were not setup inside Firebase functions.

So, the correct structure to have for the functions/ directory should be:

$ ls -a functions
.nuxt/ index.js package.json

And functions/ should have similar (not necessarily all) dependencies as the root project dir. So to add the dependencies, we can do:

yarn add dotenv firebase firebase-admin @nuxtjs/firebase @nuxtjs/axios jwt-decode nuxt nuxt-buefy

Work on Firebase Hosting

Now it’s time to think about what portion of the built Nuxt.js app are static files that should be deployed to Firebase hosting. There are also many different ways of doing this according to 45. But after trial and error, I found out that only the .nuxt/dist/client/ directory needs to be copied to the public/_nuxt/ directory that is waiting to be deployed to Firebase Hosting.

So, the correct directory structure for Firebase Hosting should be:

$ ls -a public
# _nuxt/* should be copied from .nuxt/dist/client/*
_nuxt/ favicon.ico

Wrapping up

Deploy with one command

So, to wrap it up, by using package.json and firebase.json, we can copy around necessary files and accomplish a one line command for deployment:

package.json:

  "scripts": {
    "dev": "yarn install && yarn --cwd functions install && cross-env NODE_ENV=development HOST=0.0.0.0 PORT=8080 nodemon src/server/index.js --watch src/server",
    "build": "yarn install && yarn --cwd functions install && nuxt build && rm -rf functions/.nuxt && mv .nuxt functions && rm -rf public && mkdir public && cp -r functions/.nuxt/dist/client public/_nuxt && cp -a src/static/. public/",
    "start": "cross-env NODE_ENV=production node src/server/index.js",
    "ssr": "firebase serve",
    "preparedev": "cp firebase.json.development firebase.json && cp firebase_key.json.development firebase_key.json && cp .env.development .env && cp -f firebase_key.json .env functions/",
    "prepareprod": "cp firebase.json.production firebase.json && cp firebase_key.json.production firebase_key.json && cp .env.production .env && cp -f firebase_key.json .env functions/",
    "stage": "cp firebase.json.development firebase.json && firebase use development && firebase deploy",
    "deploy": "cp firebase.json.production firebase.json && firebase use production && firebase deploy",
    "clean": "rm -rf public/ node_modules/ functions/node_modules .nuxt functions/.nuxt"
  },

firebase.json.{development,production}: They are two files, just differ by the dev or prod predeploy step.

{
  "functions": {
    "source": "functions",
    "predeploy": [
      "yarn prepare{dev,prod} && yarn --cwd functions install && yarn install && yarn build"
    ]
  },
  "hosting": {
    "predeploy": [
      "rm -rf public && mkdir public && cp -r functions/.nuxt/dist/client public/_nuxt && cp -a src/static/. public/"
    ],
    "public": "public/",
    "ignore": ["firebase.json*", "**/.*", "**/node_modules/**"],
    "rewrites": [
      {
        "source": "**",
        "function": "nuxt",
      }
    ]
  }
}

Example Repository and Demo webapp

The example repository accompanying this tutorial is located at https://github.com/1919yuan/nuxt-firebase-example. The sample deployed webapp is at https://nuxt-firebase-example-1919yuan.web.app.

Tips for Debugging

Run feature development with yarn dev

The yarn dev script setup the universal Nuxt.js app with a single process. This comes with hot reload that enables you to iterate on feature development quickly.

Emulate Firebase environment with yarn ssr

This yarn ssr command is essentially firebase serve, which starts Firebase functions emulator and a static webserver together. This is the closest environment to Firebase you can get without actually deploying. It is useful for debugging issues that only arise in SSR mode, like cookie out-of-sync issues. The drawback of this mode is that changes in src/ cannot be hot-reloaded.

ERROR Your API key is invalid, please check you have copied it correctly

This is the bug that bothered me for the longest time. There may be multiple issues that can cause this error message to come about. Some examples are:

It may be a headache that sometimes a cookie you set in client side doesn’t propagate to Firebase functions (i.e. the server side). You can only debug such issue using firebase serve and try printing out the cookies at different code locations. cookie-universal-nuxt is the solution for me. Using that package, the __session cookie I set appears on both server and client side.

This issue was noted in Austin’s article 1, where he pointed out that if hosting on Firebase functions, only the cookie with name __session goes together with the http request to Firebase functions.

buildDir in nuxt.config.js

There are some tutorials using the buildDir option to output the .nuxt build directory out to functions, but I had the same observation with 5, that if the buildDir is not the default .nuxt directory, lots of UI / functions simply don’t work. Therefore we have to move it with the build scripts.

_nuxt directory

Only the .nuxt/dist/client directory is relevant for Firebase hosting. There are some sample code that moves the entire .nuxt/ directory to Firebase hosting, that is not necessary.

References


  1. Using Firebase Authentication in a Nuxt Server-side Rendered Application ↩︎

  2. Nuxt Firebase ↩︎

  3. Vue.js/Nuxt on Firebase hosting ↩︎

  4. Deploy nuxt on Firebase ↩︎

  5. How to host Nuxt.js application on firebase with a single command ↩︎

  6. williamchong007/nuxt-ssr-firebase ↩︎