0

I have a Typescript Nx + Next.js App running with Firebase (and Firebase Admin). Within this codebase I have a firebase admin util defined as follows -

// File ./utils/FirebaseAdmin.ts
// import checkConfig from './checkConfig';
import firebaseAdmin, { cert } from 'firebase-admin/app';
import 'firebase-admin/auth';
import 'firebase-admin/firestore';
import { getAuth } from 'firebase-admin/auth';
import { getFirestore } from 'firebase-admin/firestore';

let FirebaseAdmin;

const credentials = {
  projectId: process.env.NEXT_PUBLIC_FIREADMIN_PROJECT_ID,
  privateKey: process.env.NEXT_PUBLIC_FIREADMIN_PRIVATE_KEY,
  clientEmail: process.env.NEXT_PUBLIC_FIREADMIN_CLIENT_EMAIL,
};

const options = {
  credential: cert(credentials),
};

if (firebaseAdmin.getApps().length === 0) {
  // checkConfig(credentials, true);
  FirebaseAdmin = firebaseAdmin.initializeApp(options);
}

// export const FirebaseAdmin = getApp();
export const FirebaseAdminAuth = getAuth(FirebaseAdmin);
export const FirestoreAdmin = getFirestore(FirebaseAdmin);

export default FirebaseAdmin;

But when using this either within the page getServerSideProps function or when using this in the nextjs API as follows -

// ./pages/api/projects.ts
import FirebaseAdmin from '../utils/FirebaseAdmin';  // <- at the start of the file
....
....
const SomeMethod = (handler) => {
   ....
   const adminAuth = getAuth(FirebaseAdmin);
   ....
}
export default SomeMethod;

or when even when referencing FirestoreAdmin from the util I get the below error -

 ReferenceError: Cannot access '__WEBPACK_DEFAULT_EXPORT__' before initialization
    at Module.default (webpack-internal:///./utils/FirebaseAdmin.ts:6:42)

After trying various approaches, and hours of googling around, I am still not close to a solution, this seems like some sort of execution order issue. But I could be wrong. Any clue as to what could be causing this issue or known solution would be very helpful.

Some Package Versions used -

    "next": "12.0.0",
    "firebase": "^9.5.0",
    "firebase-admin": "^10.0.0",
Shalom Sam
  • 1,539
  • 1
  • 16
  • 26

2 Answers2

1

firebase-admin/<package> imports behave like the modular SDK, where they do not have a default export. They also do not mix into a global object like the legacy JavaScript Web SDK.

I recommend only importing the "firebase-admin/app" package and leaving the others on a "as-needed" basis as you can just call getFirestore(app) and getAuth(app) when you need them.

// ./lib/firebase-admin.ts
import { initializeApp } from "firebase-admin/app";

const credentials = { /* ... */ };

export const defaultApp = initializeApp({
  credential: cert(credentials)
});

export default defaultApp;

Now I haven't compiled the Admin SDK with Webpack for Next, so if you need to guard against calling initializeApp multiple times (you shouldn't?), use the following instead:

// ./lib/firebase-admin.ts
import { initializeApp, getApp, getApps } from "firebase-admin/app";

export const defaultApp = (() => {
  if (getApps().length > 0)
    return getApp(); // returns the existing default instance

  const credentials = { /* ... */ };

  return initializeApp({
    credential: cert(credentials)
  });
})();

export default defaultApp;

In either case, you would use it as follows:

// usage
import adminApp from "./lib/firebase-admin"; // or import { defaultApp } from "./lib/firebase-admin";
import { getAuth } from "firebase-admin/auth";
import { getFirestore } from "firebase-admin/firestore";

const firestore = getFirestore(adminApp);

// do something

Addendum

Modular syntax:

// Modular Firebase Web SDK (v9+)
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";

const defaultApp = initializeApp(/* ... */);
const defaultAuth = getAuth(defaultApp);
const defaultFirestore = getFirestore(defaultApp);

export {
  defaultApp as app, // these are class instances
  defaultAuth as auth,
  defaultFirestore as firestore // i.e. you can't use firestore.doc(), import it using `import { doc } from "firebase/firestore"` and then call `doc(firestore, ...)`
}
// Modular Firebase Admin Node SDK (v10+)
import { initializeApp } from "firebase-admin/app";
import { getAuth } from "firebase-admin/auth";
import { getFirestore } from "firebase-admin/firestore";

const defaultApp = initializeApp(/* ... */);
const defaultAuth = getAuth(defaultApp);
const defaultFirestore = getFirestore(defaultApp);

export {
  defaultApp as app, // these are class instances
  defaultAuth as auth,
  defaultFirestore as firestore // i.e. you can't use firestore.doc(), import it using `import { doc } from "firebase/firestore"` and then call `doc(firestore, ...)`
}

Legacy syntax:

// Legacy Firebase Web SDK (v8 or older)
import firebase from "firebase/app";
import "firebase/auth"; // mix Auth API into firebase global
import "firebase/firestore"; // mix Firestore API into firebase global

const defaultApp = firebase.initializeApp(/* ... */);
const defaultAuth = firebase.auth(defaultApp);
const defaultFirestore = firebase.firestore(defaultApp);

export {
  firebase, // the firebase object & also the namespace
  defaultApp as app, // the rest are just class instances - not namespaces!
  defaultAuth as auth,
  defaultFirestore as firestore // i.e. no firestore.Timestamp, you must use firebase.firestore.Timestamp
}
// Legacy Firebase Admin Node SDK (v10 and older)
import * as admin from "firebase-admin";

const defaultApp = admin.initializeApp(/* ... */);
const defaultAuth = admin.auth(defaultApp);
const defaultFirestore = admin.firestore(defaultApp);

export {
  admin, // the admin object & also the namespace
  defaultApp as app, // the rest are just class instances - not namespaces!
  defaultAuth as auth,
  defaultFirestore as firestore // i.e. no firestore.Timestamp, you must use admin.firestore.Timestamp
}
samthecodingman
  • 23,122
  • 4
  • 30
  • 54
  • thanks for the quick reply. I was originally using the modular approach as proposed by the firebase docs and similar to what you have suggested here. But I still get the same error. Just to be sure I even implemented your approach mention here again. – Shalom Sam Jan 09 '22 at 13:01
0

For any one else facing similar issues, what finally seemed to work for me was enabling experimental topLevel await (in next.config.js) -

// eslint-disable-next-line @typescript-eslint/no-var-requires
const withNx = require('@nrwl/next/plugins/with-nx');

/**
 * @type {import('@nrwl/next/plugins/with-nx').WithNxOptions}
 **/
const nextConfig = {
  nx: {
    // Set this to true if you would like to to use SVGR
    // See: https://github.com/gregberge/svgr
    svgr: false,
  },
  webpack: (config) => {
    config.experiments = { topLevelAwait: true };
    return config;
  },
};

module.exports = withNx(nextConfig);

And then wrapped the Firebase initialization in a async-await IIFE (Immediately-invoked Function Expression) -

import {
  initializeApp,
  getApps,
  getApp,
  AppOptions,
  cert,
  ServiceAccount,
} from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';
import { getFirestore } from 'firebase-admin/firestore';
import checkConfig from './checkFirebaseConfig';

const FirebaseAdmin = await (async () => {
  const credentials: ServiceAccount = {
    projectId: process.env.NEXT_PUBLIC_FIREADMIN_PROJECT_ID,
    privateKey: process.env.NEXT_PUBLIC_FIREADMIN_PRIVATE_KEY.replace(
      /\\n/g,
      '\n'
    ),
    clientEmail: process.env.NEXT_PUBLIC_FIREADMIN_CLIENT_EMAIL,
  };

  const options: AppOptions = {
    credential: cert(credentials),
  };

  function createFirebaseAdminApp(config: AppOptions) {
    if (getApps().length === 0) {
      checkConfig(credentials, true);
      return initializeApp(config);
    } else {
      return getApp();
    }
  }

  const FirebaseAdmin = createFirebaseAdminApp(options);

  return FirebaseAdmin;
})();

const FirebaseAdminAuth = getAuth(FirebaseAdmin);
const FirestoreAdmin = getFirestore(FirebaseAdmin);

if (process.env.NODE_ENV === 'test') {
  process.env['FIRESTORE_EMULATOR_HOST'] = 'localhost:8080';
  FirestoreAdmin.settings({
    host: 'localhost:8080',
    ssl: false,
  });
}

export { FirebaseAdmin, FirebaseAdminAuth, FirestoreAdmin };

Not sure if this is the best approach, but it seems to work for now.

Shalom Sam
  • 1,539
  • 1
  • 16
  • 26