Firebase 9.0.0 was published to npm on August 25, 2021, and the current major is 13.0.0. Every release since 9 ships two API surfaces in the same firebase package: the modular API, built for tree shaking, and the namespaced compat libraries, which are API compatible with v8 code. Firebase says the namespaced API will not get new feature development, that compat is a temporary bridge to be removed in a future major, and that tree shaking with the modular SDK can make some apps around 80% smaller, provided you use a module bundler. This guide follows Firebase's upgrade guide and its Cloud Firestore and Authentication docs, linked at the end.
What changed
- Imports name functions instead of loading side-effect modules:
import { getFirestore, doc, getDoc } from "firebase/firestore"replacesimport "firebase/firestore". firebase.initializeApp(config)becomesinitializeApp(config)fromfirebase/app, and services come fromgetAuth()andgetFirestore().- Methods become free functions that take the service or reference first:
auth.onAuthStateChanged(cb)becomesonAuthStateChanged(auth, cb). - Firestore references are built with
doc(db, "cities", "SF")andcollection(db, "cities"), then read withgetDoc,getDocsandonSnapshot. - Writes use
setDoc,addDocandupdateDoc, andfirebase.firestore.FieldValue.serverTimestamp()becomesserverTimestamp(). DocumentSnapshot.existsis a method now:snapshot.exists().getRedirectResultreturnsnullwhen no redirect happened, instead of a credential with anulluser.- The
firebase/compat/*imports run v8-style code on the new package while you migrate.
Migration steps
Install the new SDK and switch imports to compat
npm install firebase@13.0.0Before
import firebase from "firebase/app"; import "firebase/auth"; import "firebase/firestore";After
import firebase from "firebase/compat/app"; import "firebase/compat/auth"; import "firebase/compat/firestore";Compat code is identical to v8 code; only the imports change. This is a good checkpoint to ship on its own: the app runs on the new package, and you can refactor the rest in small pieces. Do not expect a smaller bundle yet. Firebase says compat libraries have little to no size or performance advantage over the namespaced ones.
Refactor one product at a time, starting with Authentication
Before
const auth = firebase.auth(); auth.onAuthStateChanged((user) => { // check for user status }); await auth.signInWithEmailAndPassword(email, password);After
import { getAuth, onAuthStateChanged, signInWithEmailAndPassword } from "firebase/auth"; const auth = getAuth(); onAuthStateChanged(auth, (user) => { // check for user status }); await signInWithEmailAndPassword(auth, email, password);getAuth()with no argument uses the default app, andgetAuth(app)takes an explicit one. Because compatfirebase.initializeApp()sets up state for both APIs, modular code like this works while initialization is still in compat style. Firebase's suggested order is to finish one product, optionally remove that product's compat import to bank its size savings, then move to the next.Before
const result = await auth.getRedirectResult(); if (result.user === null && result.credential === null) { return null; }After
const result = await getRedirectResult(auth); if (result === null || provider.credentialFromResult(result) === null) { return null; }getRedirectResult returns null when there was no redirect. Rewrite Firestore reads and queries
Before
const db = firebase.firestore(); const snap = await db.collection("cities").doc("SF").get(); if (snap.exists) { console.log(snap.data()); } const results = await db.collection("cities").where("capital", "==", true).get();After
import { getFirestore, doc, getDoc, collection, query, where, getDocs } from "firebase/firestore"; const db = getFirestore(); const snap = await getDoc(doc(db, "cities", "SF")); if (snap.exists()) { console.log(snap.data()); } const q = query(collection(db, "cities"), where("capital", "==", true)); const results = await getDocs(q);Queries are built from free functions rather than chains:
query()takes a collection reference followed by constraints such aswhere. Query snapshots still exposeforEach,idanddata()the way they did before.Rewrite writes and field values
Before
await db.collection("cities").doc("LA").set({ name: "Los Angeles", state: "CA" }); await db.collection("cities").doc("BJ").set({ capital: true }, { merge: true }); const ref = await db.collection("cities").add({ name: "Tokyo", country: "Japan" }); await db.collection("objects").doc("some-id").update({ timestamp: firebase.firestore.FieldValue.serverTimestamp(), });After
import { doc, setDoc, addDoc, updateDoc, collection, serverTimestamp } from "firebase/firestore"; await setDoc(doc(db, "cities", "LA"), { name: "Los Angeles", state: "CA" }); await setDoc(doc(db, "cities", "BJ"), { capital: true }, { merge: true }); const ref = await addDoc(collection(db, "cities"), { name: "Tokyo", country: "Japan" }); await updateDoc(doc(db, "objects", "some-id"), { timestamp: serverTimestamp(), });set,addandupdatemap one to one ontosetDoc,addDocandupdateDoc, and the{ merge: true }option keeps its shape. Values that hung offfirebase.firestore.FieldValueare imported directly fromfirebase/firestore.Move listeners to onSnapshot
Before
const unsubscribe = db.collection("cities").doc("SF") .onSnapshot((doc) => { console.log("Current data: ", doc.data()); });After
import { doc, onSnapshot } from "firebase/firestore"; const unsubscribe = onSnapshot(doc(db, "cities", "SF"), (doc) => { console.log("Current data: ", doc.data()); });onSnapshotstill returns a function that detaches the listener, so cleanup code such asunsubscribe()in a React effect does not change.Mix styles while you migrate
Compat and modular code can live in the same file and even work on the same objects, as long as the compat packages are imported. A reference created the old way can be passed to a modular function:
import firebase from "firebase/compat/app"; import "firebase/compat/firestore"; import { getDoc } from "firebase/firestore"; const docRef = firebase.firestore().doc("cities/SF"); const snap = await getDoc(docRef);That lets you migrate a large Firestore layer function by function instead of in one change. The app compiles throughout, but none of the size benefit arrives until the compat imports are gone.
Update initialization last, then remove compat
Before
import firebase from "firebase/compat/app"; firebase.initializeApp({ /* config */ });After
import { initializeApp } from "firebase/app"; const firebaseApp = initializeApp({ /* config */ });Order matters here. Compat
firebase.initializeApp()initializes global state for both APIs, but modularinitializeApp()initializes only the modular side, so switching it early breaks every compat call that is left. Once nothing uses compat, delete everyfirebase/compat/*import; Firebase's end state has no references to thefirebase.*namespace at all. Apps loading Firebase from<script>tags on gstatic.com can use the compat scripts as a stepping stone, but Firebase does not recommend thewindow.firebaseapproach long term because it prevents unused code from being removed.
How to find every affected call
Namespaced code always starts from the firebase object or a service instance, so ast-grep finds it reliably, including chains split across lines:
sg run --pattern 'firebase.initializeApp($$$ARGS)' --lang ts
sg run --pattern 'firebase.firestore()' --lang ts
sg run --pattern 'firebase.auth()' --lang ts
sg run --pattern '$DB.collection($$$ARGS)' --lang ts
sg run --pattern '$REF.onSnapshot($$$ARGS)' --lang ts
sg run --pattern 'firebase.firestore.FieldValue.$METHOD($$$ARGS)' --lang ts
sg run --pattern '$SNAP.exists' --lang tsRepeat with --lang tsx and --lang js. $DB.collection and $SNAP.exists match other libraries and already-migrated exists() calls too, so review each hit. For imports, search your source for firebase/compat/ and for side-effect imports such as import "firebase/firestore"; ast-grep matches string literals exactly, quote style included.
Testing the change
- Run
tsc --noEmitwith strict null checks. TypeScript reports a condition likeif (snap.exists)as always true onceexistsis a function, which catches the one change that fails silently. - Fix mocks. A
jest.mockoffirebase/appthat returns{ firestore: () => fakeDb }stops intercepting once code imports functions fromfirebase/firestore; mock those functions or run tests against Firebase's local emulators. - Test the sign-in redirect path with and without a pending redirect, since
getRedirectResultnow returnsnullin the second case. - After removing compat, check your bundle analyzer output for leftover compat chunks. One forgotten import keeps the whole namespaced layer in the bundle.
Automating it
We found no official codemod for this migration. Firebase's upgrade guide describes a manual, product-by-product refactor, with the compat libraries keeping the app running between steps.
Versyka finds the namespaced call sites deterministically with ast-grep patterns like the ones above, rewrites them along with the code that depends on them (snapshot checks, listeners, mocks), then runs your repository's install, typecheck, build and tests in a disposable sandbox. A pull request is opened only when those pass.
Was this guide helpful?
Check your own repository
Paste your package.json, requirements.txt or pyproject.toml into the free upgrade check to see which API SDKs are behind, with no account. Or let Versyka run this migration on one repository and open a pull request that has already passed your tests.