Use Firebase in dynamic web apps with SSR (Server-side Rendering)

If you have worked with the Firebase JS SDK or other Firebase client SDKs, you are probably familiar with the FirebaseApp interface and how to use it to configure app instances. To facilitate similar operations on the server side, Firebase provides FirebaseServerApp.

FirebaseServerApp is a variant of FirebaseApp for use in server-side rendering (SSR) environments. It includes tools to continue Firebase sessions that span the client side rendering (CSR) / server-side rendering divide. These tools and strategies can help enhance dynamic web apps built with Firebase and deployed in Google environments like Firebase App Hosting.

Use FirebaseServerApp to:

  • Execute server-side code within the user context, in contrast to the Firebase Admin SDK which has full administration rights.
  • Enable the use of App Check in SSR environments.
  • Continue a Firebase Auth session that was created in the client.

The FirebaseServerApp lifecycle

Server-side rendering (SSR) frameworks and other non-browser runtimes such as cloud workers optimize for initialization time by reusing resources across multiple executions. FirebaseServerApp is designed to accommodate these environments by using a reference count mechanism. If an app invokes initializeServerApp with the same parameters as a previous initializeServerApp, it receives the same FirebaseServerApp instance that was already initialized. This cuts down on unnecessary initialization overhead and memory allocations. When deleteApp is invoked on a FirebaseServerApp instance, it reduces the reference count, and the instance is freed after the reference count reaches zero.

Cleaning up FirebaseServerApp instances

It can be tricky to know when to call deleteApp on a FirebaseServerApp instance, especially if you are running many asynchronous operations in parallel. The releaseOnDeref field of the FirebaseServerAppSettings helps simplify this. If you assign releaseOnDeref a reference to an object with the lifespan of the request's scope (for example, the headers object of the SSR request), the FirebaseServerApp will reduce its reference count when the framework reclaims the header object. This automatically cleans up your FirebaseServerApp instance.

Here's an example usage of releaseOnDeref:

/// Next.js
import { headers } from 'next/headers'
import { FirebaseServerAppSettings, initializeServerApp} from "firebase/app";

export default async function Page() {
  const headersObj = await headers();
  let appSettings: FirebaseServerAppSettings = {};
  appSettings.releaseOnDeref = headersObj;
  const serverApp = initializeServerApp(firebaseConfig, appSettings);
  ...
}

Resume authenticated sessions created on the client

When an instance of FirebaseServerApp is initialized with an Auth ID token, it enables bridging of authenticated user sessions between the client-side rendering (CSR) and server-side rendering (SSR) environments. Instances of the Firebase Auth SDK initialized with a FirebaseServerApp object containing an Auth ID token will attempt to sign in the user on initialization without the need for the application to invoke any sign-in methods.

Providing an Auth ID token allows apps to use any of Auth's sign-in methods on the client, ensuring that the session continues on the server-side, even for those sign-in methods that require user interaction. Additionally, it enables the offloading of intensive operations to the server such as authenticated Firestore queries, which should improve your app's rendering performance.

/// Next.js
import { initializeServerApp } from "firebase/app";
import { getAuth } from "firebase/auth";

// Replace the following with your app's
// Firebase project configuration
const firebaseConfig = {
  // ...
};

const firebaseServerAppSettings = {
  authIdToken: token  // See "Pass client tokens to the server side
                      // rendering phase" for an example on how transmit
                      // the token from the client and the server.
}

const serverApp =
  initializeServerApp(firebaseConfig,
                      firebaseServerAppSettings);
const serverAuth = getAuth(serverApp);

// FirebaseServerApp and Auth will now attempt
// to sign in the current user based on provided
// authIdToken.

Use App Check in SSR environments

App Check enforcement relies on an App Check SDK instance that Firebase SDKs use to internally call getToken. The resulting token is then included in requests to all Firebase services, allowing the backend to validate the app.

However, because the App Check SDK needs a browser to access specific heuristics for app validation, it can't be initialized in server environments.

FirebaseServerApp provides an alternative. If a client-generated App Check token is provided during FirebaseServerApp initialization, it will be used by the Firebase product SDKs when invoking Firebase services, eliminating the need for an App Check SDK instance.

/// Next.js
import { initializeServerApp } from "firebase/app";

// Replace the following with your app's
// Firebase project configuration
const firebaseConfig = {
  // ...
};

const firebaseServerAppSettings = {
  appCheckToken: token // See "Pass client tokens to the server side
                       // rendering phase" for an example on how transmit
                       // the token from the client and the server.
}

const serverApp =
  initializeServerApp(firebaseConfig,
                      firebaseServerAppSettings);

// The App Check token will now be appended to all Firebase service requests.

Pass client tokens to the server-side rendering phase

To transmit authenticated Auth ID tokens (and App Check tokens) from the client to the server-side rendering (SSR) phase, use a service worker. This approach involves intercepting fetch requests that trigger SSR and appending the tokens to the request headers.

Refer to Session management with service workers for a reference implementation of a Firebase Auth service worker. Also see Server side changes for code that demonstrates how to parse these tokens from the headers for use in FirebaseServerApp initialization.

Use Firestore in SSR environments

When building web applications with Server-side Rendering (SSR), you often need to share data between the server and the client to optimize performance and user experience. The Firestore SDK provides serialization tools that allow you to capture snapshots and specific data types on the server and pass them directly to your client-side components. This process eliminates redundant fetches by enabling the client to hydrate state using the data pre-fetched during the SSR phase. Additionally, you can transition from these serialized states to real-time listeners, ensuring your application remains synchronized with the database.

This section describes how to reuse data retrieved during the server-side rendering (SSR) phase within client-side components.

Serialize Data Types

Certain Firestore data types provide a toJSON method to convert their data into a serializable format. These include instances of objects such as Bytes, GeoPoint, Timestamp and VectorValue.

Once you have the data in JSON format, you can pass it from the server to the client through standard framework mechanisms, or as parameters to components that span the divide. For example:

import {
  Bytes
} from 'firebase/firestore';

const BYTES_DATA = new Uint8Array([0, 1, 2, 3, 4, 5]);
const bytes = Bytes.fromUint8Array(BYTES_DATA);
const bytesJSON = bytes.toJSON();

Deserialize Data Types

Firestore data types include the static method fromJSON to convert the serialized data to an operable Firestore data type.

For example, the following deserializes a Bytes data type:

import {
  Bytes
} from 'firebase/firestore';

// Assuming the same `bytesJSON` variable from the previous example.
const deserializedBytes = Bytes.fromJSON(bytesJSON);

Serialize and deserialize Firestore Snapshots

Similar to Firestore data types, you can serialize instances of DocumentSnapshot and QuerySnapshot using toJSON. However, to deserialize them, you must use the standalone functions documentSnapshotFromJSON and querySnapshotFromJSON instead of a static fromJSON method.

For example, the querySnapshot results of a query operation can be serialized using the toJSON method:

import {
  collection,
  getDocs,
  query,
  querySnapshotFromJSON
} from 'firebase/firestore';
// Assuming a configured instance of Firestore in the variable `firestore`.
const queryRef = query(collection(firestore, QUERY_PATH));
const querySnapshot = await getDocs(queryRef);
const querySnapshotJson = querySnapshot.toJSON();

Then, this data can be deserialized:

import {
  querySnapshotFromJSON
} from 'firebase/firestore';

// deserializedSnapshot is an object of type QuerySnapshot:

const deserializedSnapshot =
  querySnapshotFromJSON(firestore, querySnapshotJson);

Listeners with Serialized Snapshots

Although data queried during the SSR phase is valuable for your initial CSR render, you might still need to monitor the Firestore service for real-time updates to that information.

If your app requires these real-time updates, you can use the onSnapshotResume function to initialize Firestore SnapshotListeners with serialized Snapshot data. For example:

const observer = {
  next: (qs) => {
    console.log("onSnapshot invoked: ", qs.data());
  },
  error: (e) => {
    console.log("error callback invoked: ", e.toString());
  }
};
const unsubscribe = onSnapshotResume(firestore, querySnapshotJson, observer);