Home
> Firebase Now Works on the Server
Get updates on future FREE course and blog posts!
Subscribe

Firebase Now Works on the Server

10 min read

Jonathan Gamble

jdgamble555 on Sunday, August 18, 2024 (last modified on Sunday, August 18, 2024)

When Firebase was released, before Google even owned the company, you didn’t need to fetch anything on the server. Over 10 years later, you can pre-fetch your data to Firebase on the server before loading it on the client. This is faster, smaller, and less painful.

TL;DR#

You can now use the firebase-js-sdk library directly on the server without needing the firebase-admin package. You can fetch Firestore data that requires user credentials and normal fetching. You can add service workers to your app that add the login token to every fetch. The fetch logins on the server with initializeServerApp.

Firestore Lite#

Firebase has technically worked on the server for a long time, but not without headaches. Firebase Admin can load your data, but only on Node servers. Firebase Lite allows you to run Firebase on Edge Servers, including NextJS Middleware, Cloudflare, Bun, and Vercel.

Remember to use firebase/firestore/lite if you’re on the edge for importing Firestore.

Firebase Authentication on the Server#

The problem has been using authentication. There has not been a way to authenticate your data on the server that works across platforms. Since Firebase 10.12.3, everything works as expected, even on the Edge.

SvelteKit#

This example uses SvelteKit, but I will update all framework examples over time. First, make sure to follow the SvelteKit Todo App with Firebase to understand the Svelte setup.

Firebase Lite#

Here, we create a separate server file to handle the server request. This will be identical in all frameworks except loading the .env config and using error handling.

	// lib/firebase-lite.ts

import { PUBLIC_FIREBASE_CONFIG } from "$env/static/public";
import { error } from "@sveltejs/kit";
import { initializeServerApp } from "firebase/app";
import { getFirestore } from "firebase/firestore/lite";
import { getAuth } from "firebase/auth";

const firebase_config = JSON.parse(PUBLIC_FIREBASE_CONFIG);

export const firebaseServer = async (request: Request) => {

    const authIdToken = request.headers.get('Authorization')?.split('Bearer ')[1];

    if (!authIdToken) {
        error(401, 'Not Logged In!');
    }

    const serverApp = initializeServerApp(firebase_config, {
        authIdToken
    });

    const auth = getAuth(serverApp);

    const db = getFirestore(serverApp);

    await auth.authStateReady();

    if (auth.currentUser === null) {
        error(401, 'Invalid Token');
    }

    return {
        auth,
        db
    };
};

The initializeServerApp is key. This allows us to create a temporary server app from an ID Token. We must get it from the request first. Then we await the authorization state before getting the current user.

The request object is passed to our firebaseServer function.

	// routes/about/+page.server.ts

import { getAbout } from '$lib/about';
import { firebaseServer } from '$lib/firebase-lite';

import type { PageServerLoad } from './$types';

export const load = (async ({ request }) => {

    const { db } = await firebaseServer(request);

    return {
        about: await getAbout(db)
    };

}) satisfies PageServerLoad;

Service Worker#

The key to making this work is a Service Worker. The SW intercepts all fetch requests and adds the ID token to it. This is beautiful and doesn’t require complex session cookies.

	// service-worker/index.ts

/// <reference lib="webworker" />

import { requestProcessor } from "./utils";

declare const self: ServiceWorkerGlobalScope;

self.addEventListener('activate', (event) => {

    const evt = event as ExtendableEvent;

    evt.waitUntil(self.clients.claim())
});

self.addEventListener('fetch', (event) => {

    const evt = event as FetchEvent;

    evt.respondWith(requestProcessor(evt));
});

We want to activate our service worker as soon as it is available, and then we listen to fetch requests.

Process Request#

This is simplified code from Firebase. The Authorization Token is added to the request. I had to translate it to TypeScript. It should have been written that way first đŸ€“

	// service-worker/utils.ts

import { getIdTokenPromise } from "$lib/firebase-worker";

export const getOriginFromUrl = (url: string): string => {
    const [protocol, , host] = url.split('/');
    return `${protocol}//${host}`;
};

// Get underlying body if available. Works for text and json bodies.
export const getBodyContent = async (req: Request): Promise<BodyInit | null | undefined> => {

    if (req.method === 'GET') {
        return null;
    }

    try {
        if (req.headers.get('Content-Type')?.includes('json')) {
            const json = await req.json();
            return JSON.stringify(json);
        }
        return await req.text();
        
    } catch {
        return null;
    }
};

export const requestProcessor = async (event: FetchEvent) => {

    let req = event.request;

    const idToken = await getIdTokenPromise();

    if (
        self.location.origin === getOriginFromUrl(event.request.url) &&
        (self.location.protocol === 'https:' || self.location.hostname === 'localhost') &&
        idToken
    ) {
        const headers = new Headers(req.headers);
        headers.append('Authorization', 'Bearer ' + idToken);
        const body = await getBodyContent(req);

        try {
            req = new Request(req.url, {
                method: req.method,
                headers: headers,
                mode: 'same-origin',
                credentials: req.credentials,
                cache: req.cache,
                redirect: req.redirect,
                referrer: req.referrer,
                body,
            });
        } catch {
            // This will fail for CORS requests. We just continue with the fetch logic without passing the ID token.
        }
    }

    return await fetch(req);
};

Firebase in the Worker#

We need to get the ID token to pass it. onAuthStateChanged will always contain the latest token, so we convert it to a Promise.

	import { PUBLIC_FIREBASE_CONFIG } from "$env/static/public";
import { getApp, getApps, initializeApp } from "firebase/app";
import { getAuth, getIdToken, onAuthStateChanged } from "firebase/auth";

const firebase_config = JSON.parse(PUBLIC_FIREBASE_CONFIG);

const workerApp = getApps().length
    ? getApp()
    : initializeApp(firebase_config);

const auth = getAuth(workerApp);

export const getIdTokenPromise = (): Promise<string | null> => {
    return new Promise((resolve, reject) => {
        const unsubscribe = onAuthStateChanged(auth, async (user) => {
            unsubscribe();
            if (!user) {
                return resolve(null);
            }
            try {
                const idToken = await getIdToken(user);
                resolve(idToken);
            } catch (e) {
                reject(e);
            }
        }, reject);
    });
};

Hosting Caveat#

While this works on Vercel Edge, which uses Cloudflare, there is a small bug with direct Cloudflare hosting that we are still waiting for the PR. See Server App Not Logging in. It is one line of code, so hopefully, this will be repaired soon. It should work with Vercel Edge, Vercel Serverless, Netlify, etc.

Demo: Vercel Edge

Code: GitHub


Related Posts

© 2024 Code.Build