Add Firebase to a Vue 3 app with Firestore and VueFire

Firebase is one of the simplest ways to put a hosted real-time database in place. VueFire makes life easier still by mapping Firebase objects and their updates straight into Vue reactivity. Put together, the result is remarkably simple

Add Firebase to a Vue 3 app with Firestore and VueFire
Quick answer

Install firebase and vuefire, initialise the app with initializeApp and getFirestore, then read a collection with useCollection: the list updates itself. Develop against the local Firestore emulator rather than against the production project.

Firebase gives you a database, authentication and hosting with no server code to write. Plugged into Vue, it becomes a real-time backend whose changes propagate into the interface on their own. Here is how to wire it up cleanly with the modular SDK.

Firestore or Realtime Database?

Firebase offers two databases. Realtime Database is the older one: a single large JSON tree, with limited queries. Cloud Firestore organises data into collections of documents, and supports proper compound queries and indexes. For a new project, take Firestore, that is what this article does.

Installing

bash
npm install firebase vuefire

VueFire is the official layer connecting Firebase to Vue reactivity. It is not compulsory, but it saves you writing subscriptions and their cleanup by hand.

Initialising

Create the project in the Firebase console, add a web app, and copy the configuration. Keep it in a file of its own:

src/firebase/index.js
import { initializeApp } from 'firebase/app'
import { getFirestore } from 'firebase/firestore'

export const firebaseApp = initializeApp({
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: 'mon-projet.firebaseapp.com',
  projectId: 'mon-projet'
})

export const db = getFirestore(firebaseApp)
The Firebase API key is public

Despite what the name suggests, it is not a secret: it ships in the JavaScript bundle and anyone can read it. It identifies the project, it does not authorise anything. Security rests entirely on the Firestore rules and on authentication. Writing those rules is the real security work, not hiding this key.

Then plug VueFire into the application:

src/main.js
import { createApp } from 'vue'
import { VueFire } from 'vuefire'
import App from './App.vue'
import { firebaseApp } from './firebase'

createApp(App)
  .use(VueFire, { firebaseApp })
  .mount('#app')

Reading a collection

useCollection returns a reactive reference, filled and then kept up to date by Firestore:

src/components/ListeAvis.vue
<script setup>
import { collection } from 'firebase/firestore'
import { useCollection } from 'vuefire'
import { db } from '@/firebase'

const avis = useCollection(collection(db, 'avis'))
</script>

<template>
  <ul>
    <li v-for="a in avis" :key="a.id">{{ a.nom }} : {{ a.note }}/5</li>
  </ul>
</template>

That is all it takes: the v-for loop renders the collection like any other array. Open the Firebase console and edit a document: the list changes in the browser with no reload. VueFire closes the subscription when the component unmounts, which avoids the classic leak of forgotten listeners.

For a single document, useDocument follows the same logic:

javascript
import { doc } from 'firebase/firestore'
import { useDocument } from 'vuefire'

const produit = useDocument(doc(db, 'produits', '2234'))

Filtering and sorting

Queries are composed with query, where, orderBy and limit:

javascript
import { collection, limit, orderBy, query, where } from 'firebase/firestore'

const meilleursAvis = useCollection(
  query(
    collection(db, 'avis'),
    where('note', '>=', 4),
    orderBy('note', 'desc'),
    limit(10)
  )
)

A query combining a filter and a sort on different fields needs a composite index. Firestore rejects the query and prints a link in the console that creates the index in one click, it is the most useful error message in the product.

Writing

Writes go through the SDK functions, not through VueFire. Wire them to a form to create documents from the interface:

src/components/FormulaireAvis.vue
import {
  addDoc, collection, deleteDoc, doc, serverTimestamp, setDoc, updateDoc
} from 'firebase/firestore'
import { db } from '@/firebase'

// id generated by Firestore
await addDoc(collection(db, 'avis'), {
  nom: 'Damien',
  note: 5,
  creeLe: serverTimestamp()
})

// chosen id (creates or replaces)
await setDoc(doc(db, 'avis', 'avis-2234'), { nom: 'Damien', note: 5 })

// change some fields
await updateDoc(doc(db, 'avis', 'avis-2234'), { note: 4 })

// delete
await deleteDoc(doc(db, 'avis', 'avis-2234'))

serverTimestamp() lets Firestore set the timestamp server-side: the clock on the visitor’s machine is not to be trusted.

Developing against the emulator

Working straight on the production project damages your data and eats into the quota. The Firebase emulator suite runs Firestore locally.

bash
npm install -D firebase-tools
npx firebase init emulators     # tick Firestore
npx firebase emulators:start --only firestore --project demo-boutique

A project id starting with demo- tells the emulator to run with no account and no billing.

On the application side, connect the SDK to the emulator in development only:

src/firebase/index.js
import { connectFirestoreEmulator, getFirestore } from 'firebase/firestore'

export const db = getFirestore(firebaseApp)

if (import.meta.env.DEV) {
  connectFirestoreEmulator(db, '127.0.0.1', 8080)
}
The emulator needs Java 21

Since firebase-tools 15, an earlier JDK makes startup fail with the message “firebase-tools no longer supports Java version before 21”. Check with java -version.

Security rules

With no rules, the database is either closed or open to everyone. The “test” mode offered when you create it expires after thirty days, do not leave it in place in production.

firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    match /avis/{avisId} {
      // public read access
      allow read: if true;

      // writing reserved for signed-in users, on their own review
      allow create: if request.auth != null
                    && request.resource.data.auteurId == request.auth.uid;
      allow update, delete: if request.auth != null
                    && resource.data.auteurId == request.auth.uid;
    }
  }
}

Rules are written and tested against the emulator too, which saves you discovering a hole in production.

What changed since SDK 8

Plenty of examples still online use the old namespaced API. It has not worked as written since Firebase 9:

javascript
// old SDK (8 and earlier) — no longer works
import firebase from 'firebase'
const db = firebase.initializeApp(config).database()
db.ref('avis').push({ nom: 'Damien' })

// modular SDK (9 and later)
import { initializeApp } from 'firebase/app'
import { addDoc, collection, getFirestore } from 'firebase/firestore'
const db = getFirestore(initializeApp(config))
await addDoc(collection(db, 'avis'), { nom: 'Damien' })

Splitting the API into imported functions is not cosmetic: it lets the bundler keep only what you actually use. On an application that only reads from Firestore, the difference in weight is obvious.

On the VueFire side, the old firebase: { … } component option and the $firebaseRefs object are gone: everything goes through useCollection and useDocument.

Going further

This setup assumes you are comfortable with Vue components and reactivity. If not, start with the Vue 3 tutorial for beginners. To share Firebase data between distant components, a Pinia store wrapping the Firestore calls is a good pattern.

Common errors

Following a tutorial written for SDK 8 firebase.initializeApp(config).database() and the global firebase object have not worked since version 9. Everything goes through imported functions.
Using the old VueFire API The firebase: { … } component option and $firebaseRefs are gone: use useCollection and useDocument.
Treating the API key as a secret It ships in the bundle and authorises nothing by itself. Security rests on the Firestore rules.
Leaving the rules in test mode They expire after thirty days and leave the database open for writing until then.
Emulator and Java version Since firebase-tools 15, a JDK earlier than 21 makes startup fail.

FirebaseJavaScriptVue.js

Damien Flandrin Web developer since 2010, creator of Gekkode and Email Impact. Every article is tested on a real project before publication. Contact
Newsletter

New tests, tutorials and projects, by e-mail.

Reproducible tests, versioned code, dated results. Never any spam.