Working with Server-Side Rendering (SSR) frameworks and the traditional Firebase JavaScript SDK can present challenges in handling Firebase Auth user sessions in both CSR (Client-Side Rendering) and SSR code. Often implementations duplicate logic on the client and the server, which can increase code complexity, introduce race conditions, and significantly increase render time while waiting for redundant asynchronous operations to complete.
To address these challenges, the Firebase Web SDK has introduced FirebaseServerApp
, a new type of FirebaseApp
object that enables client-authenticated auth sessions created in the browser to be shared with SSR rendering phases in Node.js.
FirebaseServerApp usage
The process of creating an instance of FirebaseServerApp
is comparable to creating an instance of FirebaseApp
. In fact, your application may use an instance of FirebaseServerApp
wherever a FirebaseApp
instance is used. However, there are a few additional pieces of information that can be configured in FirebaseServerApp
, most importantly a Firebase Auth user ID token.
When an instance of FirebaseServerApp
is initialized with an Auth ID token, it enables seamless 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
will attempt to log in the user on initialization without the need for the application to invoke any sign-in methods.
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: idToken // We'll explain how to get the
// idToken in the service worker
// example below.
}
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.
The use of FirebaseServerApp
allows developers to leverage any of Auth’s many sign-in methods on the client, ensuring that the session seamlessly continues on the server-side, even for those sign-in methods that require user interaction. Additionally, it enables the offloading of heavy-lifting operations to the server, such as database queries, which should improve your app’s rendering performance.
Now that we have the baseline down, let’s go over some recommendations for your app’s use of FirebaseServerApp
.
Plumbing Auth ID Tokens into the SSR phase
Our example Firebase Auth service worker facilitates the transmission of authenticated Auth ID tokens from the client to the SSR rendering phases. It works to intercept fetch requests that would trigger an SSR phase, and it appends the ID token in the request headers. In addition, it proactively attempts to refresh the token to minimize the possibility of invalid ID tokens reaching the SSR phase.
While you may write a custom solution using your chosen SSR framework to attach the User’s Auth ID token to the SSR request, we suggest at least reading over our service worker to note some of our best practices for Auth ID token management.
Await authStateReady to ensure user authentication
Standard Firebase Auth sign in operations return a promise that applications may await before continuing. This allows apps to wait for the authentication steps to resolve, and then to leverage the signed-in user for other Firebase operations (storage, database access, etc) later on in the application flow.
In contrast, the initialization of the FirebaseServerApp
returns immediately. It will attempt to sign in the user behind the scenes, which is a nice streamlined experience, but alas, there’s nothing to await.
Therefore, to ensure that your app actually has a signed in user before attempting to access databases, storage, or other operations, it’s recommended to await FirebaseAuth.authStateReady()
or to use the onAuthSateChanged
handler to detect user sign-in stemming from FirebaseServerApp
initialization.
const serverApp =
initializeServerApp(firebaseConfig,
firebaseServerAppSettings);
const serverAuth = getAuth(serverApp);
await serverAuth.authStateReady();
if (serverAuth.currentUser === null) {
// authIdToken was missing or invalid.
}
// Proceed with any Firebase operations that
// require a signed-in user.
Handling no current user after initialization
It’s crucial to verify the existence of the currentUser
in the Auth module after initialization is complete. If currentUser
is null, it could indicate one of two things: either the FirebaseServerAppSettings.authIdToken
was omitted during initialization, or the Auth service detected an invalid Auth ID token.
In these cases we recommend that your app redirects to a sign in page, as it’s probably a signal that the user either hasn’t signed in, or that their Auth session on the client has expired.
FirebaseServerApp cleanup
In SSR applications, there might be many asynchronous operations running concurrently and independent of each other. It can be challenging to determine the precise end of the rendering pipeline, which raises the question: “when should I clean up the app’s use of the FirebaseServerApp
object?”
FirebaseServerApp
uses reference counting. Creating a new FirebaseServerApp
with identical parameters will return the same FirebaseServerApp
and increment its reference count. This design allows us to reuse the FirebaseServerApp
with the same authorization header to save resources.
Invoking FirebaseServerApp.delete()
will decrement the reference count and mark the object for deletion if the reference count drops to zero. In modern SSR frameworks however, it can be hard to determine when to delete the app—there’s an option to automatically decrement the reference count when another object (say the request) is garbage collected, with the releaseOnDeref
field. Here’s an example in Next.js. Here, we assume that the Auth ID token was appended to the request headers by the example service worker mentioned above.
// Example usage in Next.js.
import { headers } from 'next/headers'
import { redirect } from 'next/navigation'
import { FirebaseServerAppSettings, initializeServerApp } from "firebase/app";
import { getAuth } from 'firebase/auth';
const firebaseConfig = {
// ...
};
export default async function Page() {
const headersList = await headers();
const authIdToken = headersList.get('authorization')?.split(' ')[1];
const serverApp = initializeServerApp(firebaseConfig, { authIdToken });
const auth = getAuth( serverApp );
await auth.authStateReady();
if( !auth.currentUser ) {
return redirect("/unauthorized");
}
// ...
}
What’s next
You can start using the FirebaseServerApp
feature starting with the 10.10.0
release of the Firebase JavaScript SDK.
We’re working on new ideas to take FirebaseServerApp further and we’d like to hear your thoughts! Visit our Firebase JavaScript SDK GitHub repository if you encounter any technical issues, and reach out to us in our Firebase UserVoice to let us know of any feature requests. Thanks!