/ astro-integrations / How to Use Firebase with Astro: Complete Guide
astro-integrations 12 min read

How to Use Firebase with Astro: Complete Guide

Step-by-step guide to integrating Firebase with your Astro website. Installation, configuration, and best practices.

How to Use Firebase with Astro: Complete Guide

Firebase gives you a database, authentication, file storage, and hosting all in one package from Google. When paired with Astro, you can build full-stack apps where static pages handle the public content and Firebase powers the dynamic features like user accounts, real-time data, and file uploads.

The official Astro backend guide documents Firebase as a first-class option, so the patterns below follow the approach Astro recommends rather than a one-off workaround.

This guide was checked against the docs on 2026-05-29 and pins the package versions that were current on that date.

Prerequisites

  • Node.js 22.12.0 or newer. Astro 6 declares "engines": { "node": ">=22.12.0" } in its package.json, so the older "Node 18+" advice you may see in tutorials no longer applies to a current Astro project. The Firebase Admin SDK separately requires Node 18 or newer, so a Node 22 runtime satisfies both.
  • An Astro project (npm create astro@latest)
  • A Firebase account and project (free Spark plan at firebase.google.com)
  • Firebase CLI installed (npm install -g firebase-tools@15.19.0)

Installation

Install the Firebase SDKs. Pinning the versions that were current when this guide was written keeps your setup reproducible:

npm install firebase@12.14.0 firebase-admin@13.10.0

The firebase package is the client SDK for browser code. The firebase-admin package handles server-side operations in API routes and .astro frontmatter. Both package names are current and published on npm.

The client SDK uses the modular, tree-shakeable API (v9 and later), which Firebase strongly recommends for production because module bundlers can eliminate the parts of the SDK you do not import. That is why each service is imported from its own entry point (firebase/app, firebase/auth, firebase/firestore) instead of a single global object.

Enable On-Demand Rendering With an Adapter

This is the step most outdated tutorials get wrong. Anything that uses the Firebase Admin SDK or reads cookies (login, session verification, request-time Firestore reads) must run on the server, which means those routes need on-demand rendering.

In Astro 6, output: 'static' is the default, and it already supports per-route on-demand rendering. The old output: 'hybrid' value was removed in Astro 5, which merged hybrid and static into a single 'static' mode that keeps the opt-out-of-prerendering capability. You do not need to set output: 'hybrid' anymore, and doing so will error.

What you do still need is an adapter so Astro has a server runtime to render on-demand routes. Add the Node adapter:

npx astro add node

That command writes the adapter into your config. The result looks like this:

// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node"; // @astrojs/node 10.1.2

export default defineConfig({
  adapter: node({
    mode: "standalone",
  }),
});

The Node adapter supports two modes: standalone (builds a server that starts when you run the entry module) and middleware (output you mount inside an existing Express or Fastify server). With the adapter in place, the whole site stays statically prerendered by default, and any page or endpoint that needs the server opts in with one line:

export const prerender = false;

If most of your app is dynamic, you can instead set output: 'server' in the config to flip the default, then mark your static pages with export const prerender = true.

Configuration

You need two separate sets of credentials. The client (web app) credentials come from the Firebase console under Project Settings > General > Your apps, clicking the Web app icon. The server (Admin SDK) credentials come from Project Settings > Service accounts > Firebase Admin SDK > Generate new private key, which downloads a service account JSON file.

Add both to your .env file. The client values are prefixed with PUBLIC_ so Astro exposes them to browser code, while the Admin SDK values stay private:

# Client-side (exposed to the browser, this is by design)
PUBLIC_FIREBASE_API_KEY=your_api_key
PUBLIC_FIREBASE_AUTH_DOMAIN=your_project.firebaseapp.com
PUBLIC_FIREBASE_PROJECT_ID=your_project_id
PUBLIC_FIREBASE_STORAGE_BUCKET=your_project.appspot.com
PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your_sender_id
PUBLIC_FIREBASE_APP_ID=your_app_id

# Server-side (private, never prefix these with PUBLIC_)
FIREBASE_PRIVATE_KEY_ID=your_key_id
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
FIREBASE_PROJECT_ID=your_project_id
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxx@your_project.iam.gserviceaccount.com
FIREBASE_CLIENT_ID=your_client_id
FIREBASE_AUTH_URI=https://accounts.google.com/o/oauth2/auth
FIREBASE_TOKEN_URI=https://oauth2.googleapis.com/token
FIREBASE_AUTH_CERT_URL=https://www.googleapis.com/oauth2/v1/certs
FIREBASE_CLIENT_CERT_URL=your_cert_url

The full set of server-side variable names above matches the service account JSON fields that the official Astro Firebase guide reads when building the credential object.

Set up the client-side Firebase initialization:

// src/lib/firebase-client.ts
import { initializeApp, getApps } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: import.meta.env.PUBLIC_FIREBASE_API_KEY,
  authDomain: import.meta.env.PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: import.meta.env.PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: import.meta.env.PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: import.meta.env.PUBLIC_FIREBASE_APP_ID,
};

const app = getApps().length ? getApps()[0] : initializeApp(firebaseConfig);

export const auth = getAuth(app);
export const db = getFirestore(app);

And the server-side admin setup. The modular Admin SDK imports initializeApp and cert from firebase-admin/app, which is the current import path. The getApps().length === 0 guard prevents re-initializing the app across requests, the same pattern the official Astro guide uses:

// src/lib/firebase-admin.ts
import type { ServiceAccount } from "firebase-admin";
import { initializeApp, cert, getApps } from "firebase-admin/app";
import { getAuth } from "firebase-admin/auth";
import { getFirestore } from "firebase-admin/firestore";

const serviceAccount = {
  type: "service_account",
  project_id: import.meta.env.FIREBASE_PROJECT_ID,
  private_key_id: import.meta.env.FIREBASE_PRIVATE_KEY_ID,
  // Firebase stores the key with literal \n sequences, so restore real newlines
  private_key: import.meta.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, "\n"),
  client_email: import.meta.env.FIREBASE_CLIENT_EMAIL,
  client_id: import.meta.env.FIREBASE_CLIENT_ID,
  auth_uri: import.meta.env.FIREBASE_AUTH_URI,
  token_uri: import.meta.env.FIREBASE_TOKEN_URI,
  auth_provider_x509_cert_url: import.meta.env.FIREBASE_AUTH_CERT_URL,
  client_x509_cert_url: import.meta.env.FIREBASE_CLIENT_CERT_URL,
};

const activeApps = getApps();
const app =
  activeApps.length === 0
    ? initializeApp({ credential: cert(serviceAccount as ServiceAccount) })
    : activeApps[0];

export const adminAuth = getAuth(app);
export const adminDb = getFirestore(app);

Basic Usage: Authentication

Here is a login form using Firebase Auth on the client side:

---
// src/pages/login.astro
export const prerender = false;
import BaseLayout from "../layouts/BaseLayout.astro";
---

<BaseLayout title="Login">
  <form id="login-form" class="max-w-md mx-auto py-12">
    <input type="email" id="email" placeholder="Email" required
           class="w-full p-3 border rounded mb-4" />
    <input type="password" id="password" placeholder="Password" required
           class="w-full p-3 border rounded mb-4" />
    <button type="submit" class="w-full p-3 bg-blue-600 text-white rounded">
      Sign In
    </button>
    <p id="error" class="text-red-500 mt-2 hidden"></p>
  </form>
</BaseLayout>

<script>
  import { signInWithEmailAndPassword } from "firebase/auth";
  import { auth } from "../lib/firebase-client";

  const form = document.getElementById("login-form") as HTMLFormElement;
  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    const email = (document.getElementById("email") as HTMLInputElement).value;
    const password = (document.getElementById("password") as HTMLInputElement).value;

    try {
      const userCredential = await signInWithEmailAndPassword(auth, email, password);
      const token = await userCredential.user.getIdToken();

      await fetch("/api/auth/session", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ token }),
      });

      window.location.href = "/dashboard";
    } catch (error) {
      const el = document.getElementById("error");
      if (el) {
        el.textContent = "Invalid email or password";
        el.classList.remove("hidden");
      }
    }
  });
</script>

Server-Side Session Verification

This is another spot where details matter. The Astro Firebase guide does not store the raw ID token in a cookie. Instead it verifies the incoming ID token, then exchanges it for a long-lived session cookie via createSessionCookie. There is also a hard naming constraint worth memorizing. When you deploy on Firebase Hosting, Firebase only forwards a single cookie to your backend and it must be named __session. Using any other name means the cookie silently disappears in production.

// src/pages/api/auth/session.ts
export const prerender = false; // server-rendered, opt out of static
import type { APIRoute } from "astro";
import { adminAuth } from "../../../lib/firebase-admin";

export const POST: APIRoute = async ({ request, cookies }) => {
  const { token } = await request.json();

  try {
    // 1. Verify the ID token the client sent
    const decodedToken = await adminAuth.verifyIdToken(token);

    // 2. Exchange it for a session cookie (5 days here)
    const fiveDays = 60 * 60 * 24 * 5 * 1000;
    const sessionCookie = await adminAuth.createSessionCookie(token, {
      expiresIn: fiveDays,
    });

    // 3. Store it. The name MUST be __session for Firebase Hosting.
    cookies.set("__session", sessionCookie, {
      httpOnly: true,
      secure: true,
      path: "/",
      maxAge: fiveDays / 1000,
    });

    return new Response(JSON.stringify({ uid: decodedToken.uid }), {
      status: 200,
    });
  } catch {
    return new Response(JSON.stringify({ error: "Invalid token" }), {
      status: 401,
    });
  }
};

To check the session on a protected route, read the __session cookie and call adminAuth.verifySessionCookie(cookie, true) (the second argument forces a check against revoked sessions).

Firestore Data Fetching

Read Firestore data straight from .astro frontmatter. The official guide uses adminDb.collection(...) syntax on the server. If the page should reflect live data on every request, add export const prerender = false so it renders on demand. Without that line the page is prerendered at build time and the data is frozen to whatever Firestore returned during the build.

---
// src/pages/products.astro
export const prerender = false; // render on demand for fresh data
import { adminDb } from "../lib/firebase-admin";
import BaseLayout from "../layouts/BaseLayout.astro";

const snapshot = await adminDb.collection("products").orderBy("name").get();
const products = snapshot.docs.map((doc) => ({
  id: doc.id,
  ...doc.data(),
}));
---

<BaseLayout title="Products">
  <ul>
    {products.map((product) => (
      <li>
        <h2>{product.name}</h2>
        <p>${product.price}</p>
      </li>
    ))}
  </ul>
</BaseLayout>

Production Tips

  1. Use the Admin SDK on the server only. The client SDK exposes your config (which is fine, it is designed for that), but the Admin SDK uses private credentials that must stay on the server. Never import firebase-admin in client-side code.

  2. Set up Firestore security rules. Default rules allow all access for 30 days. Lock them down before going to production. Use the Firebase console or deploy rules from a firestore.rules file.

  • Enable App Check. App Check verifies that requests to your Firebase backend come from your actual app, not from a script or bot. It adds a layer of protection against abuse.

  • Use composite indexes for complex queries. If you query Firestore with multiple where clauses or orderBy, you need composite indexes. Firebase will log a link to create them when a query fails.

  • Watch your read/write costs. Firestore charges per document read, write, and delete. Use pagination, cache responses, and avoid fetching entire collections on every page load.

  • Common Errors and Fixes

    output: 'hybrid' is not a valid option. Astro 5 removed it. The upgrade guide states that v5 merged output: 'hybrid' and output: 'static' into a single 'static' mode that keeps the on-demand capability. If your config still sets 'hybrid', delete that line. The default 'static' already lets individual routes opt out of prerendering.

    Pages using the Admin SDK or cookies render with stale data or fail. A route that calls firebase-admin or reads cookies needs to run on the server. Add an adapter (npx astro add node) and put export const prerender = false at the top of that page or endpoint. Without an adapter, Astro has no server runtime for on-demand routes.

    Your session cookie vanishes on Firebase Hosting. Firebase Hosting forwards only one cookie to your backend, and it must be named __session. A cookie named anything else (for example firebase-session) is dropped before it reaches your server code. Name it __session.

    Failed to parse private key from the Admin SDK. Environment files store the private key with literal \n two-character sequences instead of real newlines. Restore them with import.meta.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, "\n") before passing the key to cert().

    Node version errors during install or build. Astro 6 declares "engines": { "node": ">=22.12.0" }, so an older Node will warn or fail. The Firebase Admin SDK separately requires Node 18 or newer. Run Node 22 or later to satisfy both at once.

    The query requires an index from Firestore. Queries that combine multiple where clauses or an orderBy need a composite index. Firestore logs a direct link to create the index in the error message; follow it once and the query works thereafter.

    Importing firebase-admin in client code leaks credentials. The Admin SDK is server-only. Keep it in .astro frontmatter, API routes, and src/lib modules that the client never imports. Use the client firebase SDK in <script> blocks and components.

    Official Docs and Examples

    Alternatives to Consider

    • Supabase if you prefer PostgreSQL over NoSQL and want an open-source alternative with a similar feature set. Better for relational data.
    • Neon or Turso if you just need a database without auth and storage bundled in. Lighter and cheaper at scale.
    • Clerk if you only need authentication. It is simpler to set up than Firebase Auth and has pre-built UI components.

    Wrapping Up

    Firebase is a solid choice when you need multiple backend services under one roof. The client and admin SDKs cover both browser and server needs, and Astro's static-by-default rendering with per-route on-demand opt-out lets you use each where it makes sense. Start with the free Spark plan and scale up as your app grows.

    Sources

    All versions and facts below were checked on 2026-05-29.