Published on

Using Firebase Admin SDK with Next.js 13

Authors

I recently had to use Firestore Database and Firebase Storage for the backend of a NextJs application. This is a guide on how you can set this up. We will look at how to pull in a list of collections and their data, get a specific document by its ID, and lastly get an image from storage using the public download url.

I also show you how you can hide this public download url behind an API route for another level of obfuscation for your front-end.

Firebase Admin SDK


It's crucial to note that the Firebase Admin SDK and the Firebase SDK serve different purposes. The Firebase Admin SDK is designed for server-side environments, providing privileged access to your Firebase project's resources, such as Firebase Authentication, Realtime Database, Cloud Firestore, and more. On the other hand, the Firebase SDK is aimed at client-side development, enabling interaction with Firebase features directly from client applications. This guide is for the Admin SDK

Getting Started

The first thing you are going to need is a Firebase Project. To set this up start here. I go through this in the video above, if you do not already have one set up.

Once you have created the project, create a new web app within that project.

Once that is created, go ahead to the project settings. Here we need to download a service account private key. Go to the service accounts tab and click Generate new private key. This should download a JSON file we will use later

1. Add your environment variables

We will need to add four values to our environment variables. If you dont already have a .env file create one now. If you have one add these values in to the file:

.env
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
FIREBASE_PRIVATE_KEY=
FIREBASE_CLIENT_EMAIL=

Fill these values in with the respective values from the JSON you downloaded earlier. Storage bucket by default will be the project ID plus .appspot.com at the end. You can also find this in your project settings, under the web app configuration.

Warning:


Its worth reiterating, you do not want the contents of that JSON file to leak as someone could get access to your database with admin permissions, this is why we utilise the environment variables. The ProjectID and Storage Bucket can be on the front-end however as they are not private. This is why they are prefixed with NEXT_PUBLIC.

2. Install npm packages

We will need the firebase-admin package, so go ahead and install that.

I also reccommend installing server-only, I will explain this in section 3.

npm install firebase-admin
npm install server-only --save-dev

3. Initialise Firebase Admin

Now that we have our configuration values, we need to create a function to utilise these. Create a file called firebaseAdmin.ts. This is the file we will use to store the configuration for firebase admin.

Populate the file with the code below, I'll explain the key points:

firebaseAdmin.ts
import "server-only"
 
import admin from "firebase-admin"
 
interface FirebaseAdminAppParams {
  projectId: string
  clientEmail: string
  storageBucket: string
  privateKey: string
}
 
function formatPrivateKey(key: string) {
  return key.replace(/\\n/g, "\n")
}
 
export function createFirebaseAdminApp(params: FirebaseAdminAppParams) {
  const privateKey = formatPrivateKey(params.privateKey)
 
  if (admin.apps.length > 0) {
    return admin.app()
  }
 
  const cert = admin.credential.cert({
    projectId: params.projectId,
    clientEmail: params.clientEmail,
    privateKey,
  })
 
  return admin.initializeApp({
    credential: cert,
    projectId: params.projectId,
    storageBucket: params.storageBucket,
  })
}
 
export async function initAdmin() {
  const params = {
    projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID as string,
    clientEmail: process.env.FIREBASE_CLIENT_EMAIL as string,
    storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET as string,
    privateKey: process.env.FIREBASE_PRIVATE_KEY as string,
  }
 
  return createFirebaseAdminApp(params)
}

At the top of the file we import server-only. This adds a build time check to our project, that ensures any function run in this file is run in a server environment. This helps catch leaks into client components. Read more here.

The function initAdmin() will need to be called wherever we want to access firestore/storage. This ensures an application has been initialised. If its called and one is already initialised, it will return that instance and not create a new one.

4. Fetch data from firestore

To fetch some data from firestore, I am again going to create a new file to house our functions, with the server-only header as well. Mine will be called firebase.ts

I am going to add some example functions in here to fetch some data I have stored in the database, as well as a public download URL for a file I have stored in storage.

firebase.ts
import "server-only"
 
import { getFirestore } from "firebase-admin/firestore"
import { getDownloadURL, getStorage } from "firebase-admin/storage"
 
export const getLinks = async () => {
  const firestore = getFirestore()
  const linkSnapshot = await firestore.collection("links").get()
  const documents = linkSnapshot.docs.map((link) => ({
    url: link.data().url,
    title: link.data().title,
    desc: link.data().desc,
  }))
 
  return documents
}
 
export const getLogo = async () => {
  const firestore = getFirestore()
  const logoSnapshot = await firestore.collection("images").doc("logo").get()
  const logoData = logoSnapshot.data() as { url: string } | undefined
  if (!logoSnapshot.exists || !logoData) {
    return null
  }
  return logoData.url
}
 
export const getLogoFromStorage = async () => {
  const bucket = getStorage().bucket()
  const file = bucket.file("logo.png")
  const imageUrl = await getDownloadURL(file)
  console.log(imageUrl)
  return imageUrl
}

In the function getLogoFromStorage its worth noting the logo.png is the path to the file in firebases storage. So if you had it nested in a folder it would look something like this images/logo.png.

Further, the getLogoFromStorage function uses the firebase public download url. In Firebase, a public download URL is a unique link generated for a file stored in Firebase Cloud Storage, which allows the file to be accessed, provided they have the correct access token. The access token is a random UUIDv4 string that makes the URL hard to guess, thereby providing a level of security against unauthorized access. In a section below I explain how you can further hide this url from your users.

Lastly, these functions will not work unless we call the initAdmin function we defined earlier. Before you call these, make sure you do await initAdmin().

5. Using in Server Components

To use these in a server component, make sure the default export is an async function.

After that we need to make sure we call await initAdmin() to initalise the application set-up if it has not been already.

Then it is as simple as calling the functions to get the data we need.

page.tsx
export default async function Home() {
  await initAdmin();
  const links = await getLinks();
  const logo = "/api/image";
  return (
    // ...your react component
  )
}

6. Hiding Firebase Public Download URLs

To hide the public download URL, we are going to utilise a NextJS API route. This route will be used as the new url for the file. You could pass it additional parameters in the body to handle a range of files. For this example we are simply going to hide the URL of an image. You could also do this in any server environment that you can make requests in, such as Cloud Functions.

First create the API route, this will be a folder structure like this app/api/image/route.ts,

Next, add the following:

route.ts
import { getLogoFromStorage } from "@/db/firebase"
import { initAdmin } from "@/db/firebaseAdmin"
 
export async function GET(request: Request) {
  await initAdmin()
 
  const imageUrl = await getLogoFromStorage()
 
  const response = await fetch(imageUrl)
 
  return new Response(response.body, { headers: response.headers })
}

Again, we are calling the initAdmin function to ensure the application is initialised.

Next we are simply using the same method to get the URL, but this time it will only occur on the server.

Lastly, we fetch the URL from within the function, and we return its response as a new one.

Now when you go to /api/image it will load in the image, but with a clean URL, and the public download URL with the access token has only been exposed to the server.