Securely connecting Firebase-Firestore DB to NextJS application.

Author: Praweg Koirala

🗓️ March 8, 2024

⌛️ 8 mins

🪴 Growing

🏷️    firebase | nextjs | backend | intermediate | Programing

One of the applications I have been working on recently uses a Firestore Database for the database layer. The app is actually a Mobile app built with flutter and hence uses the Firebase SDK for all the connection and data fetching.

However, I needed to create a separate admin application that would allow admins to add data to the database. This data would be catered to in the mobile application. This admin panel would be in a Web Application that would allow data monitoring and management for several applications. Essentially, I was building a CMS for the mobile app.

Since NextJS is pretty versatile, the web app with admin panel was going to be built with NextJS. Without much thinking, I started building the frontend. There was enough documentation on how to incorporate firebase and firestore. I was able to fetch the data pretty quickly and added the feature to add and delete data as well.

With most of the features configured, I decided to push my code to my repo and get it quickly hosted and tested to see if all the features worked well.

Now came the part where I had to make sure I would not be pushing any keys and credentials to the repo and also make sure when I hosted it to make it publicly available I won’t be exposing my keys and credentials that are exposed in my client side.

This was when I realized if I am using the firebase/firestore SDK directly on the client side I will not have an option to keep my credentials truly hidden when it is hosted on the server.

I am using Vercel as my web host which allows an easy way to store your credentials in a separate environment variable storage on the server. However, any secrets that are accessed directly on the client side can only be stored as such:

NEXT_PUBLIC_FB_API_KEY=averylongsecretkeywith##8732984 NEXT_PUBLIC_FB_MESSAGING_SENDER_ID=0937509732097590327 NEXT_PUBLIC_FB_APP_ID=apikeyyoudontwannashare19792173

This is not what I was intending. I was hoping I would be able to pass these credentials like any env variables and store them in my .env file as a regular variable name (FB_API_KEY, FB_APP_ID, etc.) during development and store them in Vercel's secrets/env variable storage in production.

But it turned out that if you are accessing the secrets & env variables in client-side code, the only way you can access them is by storing them as NEXT_PUBLIC... strings.

I was a little bummed to first learn this since it slammed the brakes on my progress, but soon I realized this was a good learning opportunity and real-life experience. I switched my gear to an eager learner and enthusiastically started researching and exploring documentation. I am glad we live in a world of AI and automation, though I did not even have to go as far as Google search.

GitHub Copilot to the rescue!

I highlighted my issues and typed away to my friendly neighbor AI Bot waiting for me eagerly in my IDE (VSCode), asking why I can not pass the variable as normal without NEXT_PUBLIC... strings or how I can make sure I do not expose credentials in client-side code? It gave me great explanations and suggestions right away.

To summarize, first, it highlighted the fact that NextJS requires NEXT_PUBLIC strings if I am accessing credentials stored in env variables in the client-side code, and only server-side code can access the regular env variable strings. Secondly, it suggested me several different ways to handle server-side code in NextJS. Again, to summarize these, my options were using NextJS special functions like getServerSideProps, using server actions, or creating API routes and setting up connections and CRUD with Firestore in the API routes.

(Quick note on this... since there have been a lot of big changes in NextJS in the recent past, GitHub Copilot did not have all the information I was looking for. I was working with Next JS 14 with app router, and one of the suggestions did not work for me (getServerSideProps) because it was already depreciated. I spent some time comparing my remaining options and decided to go with creating the API routes. I really liked server actions and its ability to just use it alongside client code when working with serverless databases, but in my case, I felt like having a separate endpoint was a better choice. Having dedicated API endpoints also meant that if I wanted to integrate those in my mobile application as a REST API or elsewhere, I could do that.)

Here is how I created a secure connection with the Firebase-Firestore DB using the API routes in NextJS:

First of all, create an API folder inside the app directory.

Create separate folders for different routes and functions (GET, DELETE, POST, etc.).

Inside each of these folders, create a route.ts/route.js file.

The route.ts file will hold the server-side code to handle that particular function. For example, to Fetch Books in my app, I wrote the following code:

import { db } from '@/firebaseConfig'; import { collection, getDocs } from 'firebase/firestore'; export async function GET() { try { const booksCollection = collection(db, 'allbooks'); const booksSnapshot = await getDocs(booksCollection); const booksList = booksSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); return Response.json(booksList, { status: 200 }); } catch (error) { console.error('Error fetching books:', error); return Response.json({ error: 'Failed to fetch books', status: 500 }); } }

My API route is: /api/fsdb/fsdbBooks/fetchbook. The API directory is inside the main app directory. Here are the details of what's happening in the code above:

We're using the Firebase/Firestore SDK to establish a connection to our collection by first connecting to the database with the config details and providing the name of the collection. Once the connection is established, we're using the getDocs method and providing the collection details to get the documents in that collection. This is an asynchronous function, so we're using async/await to ensure it runs asynchronously. The map function is used to transform the array of QueryDocumentSnapshot objects into an array of plain JavaScript objects. Each object in the new array has an id property (the document ID) and all the other properties from the document's data.

If everything goes well, the function returns a JSON response with an array of books and a status code of 200. This is done using the Response.json method.

Note: The firebaseConfig import on top. By calling this config on the server side, we've ensured that the credentials in the config files are only accessed via server code and hence not exposed. Below is what the config file looks like:

import { initializeApp } from "firebase/app"; import { getFirestore } from "@firebase/firestore" // env variables with NEXT_PUBLIC is needed to access in client side // const firebaseConfig = { // apiKey: process.env.NEXT_PUBLIC_FB_API_KEY, // authDomain: process.env.NEXT_PUBLIC_FB_AUTH_DOMAIN, // projectId: process.env.NEXT_PUBLIC_FB_PROJECT_ID, // storageBucket: process.env.NEXT_PUBLIC_FB_STORAGE_BUCKET, // messagingSenderId: process.env.NEXT_PUBLIC_FB_MESSAGING_SENDER_ID, // appId: process.env.NEXT_PUBLIC_FB_APP_ID // } // The following env variables only work on the server const firebaseConfig = { apiKey: process.env.FB_API_KEY, authDomain: process.env.FB_AUTH_DOMAIN, projectId: process.env.FB_PROJECT_ID, storageBucket: process.env.FB_STORAGE_BUCKET, messagingSenderId: process.env.FB_MESSAGING_SENDER_ID, appId: process.env.FB_APP_ID } const app = initializeApp(firebaseConfig); export const db = getFirestore(app); export default app;

The actual credentials are stored in .env file in the .env file when running on local server or in the vercel server’s secrets/ env variable storage when hosted in Vercel( similar secret managers when hosted in other web hosts). In the above example you can see in the commented code how you would need to use NEXT_PUBLIC if accessing these credentials directly in client side.

Now we can securely fetch these data in client side with fetch and useEffect hook without exposing our database credentials. Below is the example of how i have done it in my code (we can opt to use useSWR and entirely skip useEffect/ useState hook to optimize our code and also make the fetch more robust, but we will save that for some other tutorial)

import { useState, useEffect } from 'react' function BooksPage() { // const { data: books, error } = useSWR('/api/fsbooks/readBooks'); const { data: session } = useSession(); const [books, setBooks] = useState([]); const [title, setTitle] = useState(''); const [language, setLanguage] = useState('English'); const [coverImage, setCoverImage] = useState(''); const [error, setError] = useState(''); const getBooks = async () => { fetch('/api/fsdb/fsdbBooks/fetchbooks') .then(res => res.json()) .then(data => setBooks(data)) .catch(error => { console.error(error); setError('Failed to fetch books'); }) } // fetching data via server side, api route available at /api/fsbooks/readBooks. useEffect(() => { getBooks(); console.log(books) }, [])

In the above code example we are using useState hooks to store our state variables , getBooks function fetches the data from our api that we previously defined in the server side . useEffect hooks makes sure to serve the fetched data when this page loads.

Here is the detail of what is happening in our client side :

The getBooks function is an asynchronous function that fetches data from the server. It uses the fetch API to make a GET request to the '/api/fsdb/fsdbBooks/fetchbooks' endpoint. Once the response is received, it is converted to JSON format using the res.json() method. The resulting data is then used to update the state of books by calling the setBooks function. If there's an error during this process, it's caught in the catch block where it's logged to the console and the setError function is called with the message 'Failed to fetch books'.

The useEffect hook is used to perform side effects in a React component. In this case, it's used to call the getBooks function when the component mounts. The useEffect hook takes two arguments: a function that contains the side-effectful code, and an array of dependencies. When any of the dependencies change, the function is run again. In this case, the dependency array is empty ([]), which means the function will only run once, right after the component mounts.

Here is the screenshot of what our UI looks like with the fetched data.

1. NextJS FrontEnd UI with data fetched through Firestore DB . Secure connection to database via api route.

Below is the Screenshot of the Android App built with Flutter. The web Application is serving as CMS to change our data realtime via firestore DB.

2 .Firestore data in Mobile(Flutter) app added via NextJS App with firestore.

Similar to Data Fetching process described above . Post, Update and Delete is also configured using separate api routes for each of those method in their route handler functions.

The Api requests are then done in client side using Fetch with its corresponding methods (POST,DELETE, PATCH etc) .

This concludes today’s session on secure connection to firestoreDB and how to avoid accidental exposure of our credential when configuring firestoreDB in our NextJS app. Hope this was a helpful tutorial. If anyone is interested in more details of any of the processes we covered today feel free to leave that in comment and i will try to cover that in future posts.

This post was last updated on: March 8, 2024

Back