How to Integrate Firebase Auth with Astro: Complete Guide
Step-by-step guide to integrating Firebase Auth with your Astro website. Setup, configuration, and best practices.
How to Integrate Firebase Auth with Astro: Complete Guide
Firebase Authentication provides backend services and SDKs for authenticating users in your application. It supports email/password, phone numbers, and popular federated identity providers like Google, Facebook, Twitter, and GitHub. When integrated with Astro, Firebase Auth enables secure user sessions that work across server-rendered pages and client-side interactions.
This guide covers a split approach where Firebase handles authentication on the client side, and you verify sessions server-side using Firebase Admin SDK for protected routes and API endpoints. Astro 6 ships static output by default, so the auth pages and endpoints below opt into on-demand rendering individually with an adapter, which is the pattern Astro now recommends after the standalone output: 'hybrid' mode was removed in Astro 5.
Prerequisites
You will need:
- Node.js v22.12.0 or newer installed (Astro 6 requires
node >=22.12.0per its publishedenginesfield) - An existing Astro project (v6.x at the time of writing)
- An on-demand rendering adapter added (this guide uses
@astrojs/node), because the login flow, the session endpoint, and protected pages must render per request - A Firebase project created at console.firebase.google.com
- Firebase Authentication enabled in your project (Authentication > Sign-in method)
- A Firebase service account key (Project Settings > Service accounts > Generate new private key)
Installation
Install both the Firebase client SDK (for browser-side auth) and the Admin SDK (for server-side verification). Both package names are current and the install command is unchanged:
npm install firebase firebase-admin
Versions verified against the npm registry on 2026-05-29: firebase is at 12.14.0 and firebase-admin is at 13.10.0. The Admin SDK declares node >=18 in its engines field, which the Astro 6 Node requirement already satisfies.
Add an on-demand rendering adapter. The npx astro add node command is still the documented way to install and wire it:
npx astro add node
That command installs @astrojs/node (current version 10.1.2) and writes the adapter into astro.config.mjs. The resulting config looks like this:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
adapter: node({
mode: 'standalone',
}),
});
You do not set output: 'server' or the removed output: 'hybrid'. Astro 6 keeps output: 'static' as the default, and you mark only the pages and endpoints that need server rendering with export const prerender = false. Every server-rendered file in this guide includes that export.
Configuration
Environment Variables
Add Firebase credentials to your .env file:
PUBLIC_FIREBASE_API_KEY=your_api_key
PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
PUBLIC_FIREBASE_PROJECT_ID=your-project-id
FIREBASE_ADMIN_PROJECT_ID=your-project-id
FIREBASE_ADMIN_CLIENT_EMAIL=firebase-adminsdk-xxxxx@your-project.iam.gserviceaccount.com
FIREBASE_ADMIN_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYOUR_KEY_HERE\n-----END PRIVATE KEY-----\n"
The PUBLIC_ prefixed variables are safe for the client. The admin credentials must stay server-side only. Astro reads these through import.meta.env, which is how the code below accesses them. If you want stronger guarantees about which variables are public versus secret, Astro also offers the type-safe astro:env API (available since Astro 5.0 and current in v6), where you would declare the admin keys as context: 'server', access: 'secret' in your config and read them with getSecret() from astro:env/server. The import.meta.env approach used here still works; astro:env is the more explicit, schema-validated alternative.
Client-Side Firebase Setup
Create a client-side Firebase configuration:
// src/lib/firebase-client.ts
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
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,
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
Server-Side Firebase Admin Setup
Initialize the Admin SDK for token verification:
// src/lib/firebase-admin.ts
import { initializeApp, cert, getApps } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';
const activeApps = getApps();
const app = activeApps.length === 0
? initializeApp({
credential: cert({
projectId: import.meta.env.FIREBASE_ADMIN_PROJECT_ID,
clientEmail: import.meta.env.FIREBASE_ADMIN_CLIENT_EMAIL,
privateKey: import.meta.env.FIREBASE_ADMIN_PRIVATE_KEY?.replace(/\\n/g, '\n'),
}),
})
: activeApps[0];
export const adminAuth = getAuth(app);
The getApps() check prevents reinitializing the app on hot reload during development.
Common Patterns
Client-Side Login Component
Create a login form that authenticates with Firebase and stores the session token in a cookie:
---
// src/pages/login.astro
export const prerender = false;
---
<html>
<body>
<h1>Log In</h1>
<form id="login-form">
<label>Email <input type="email" id="email" required /></label>
<label>Password <input type="password" id="password" required /></label>
<button type="submit">Log In</button>
</form>
<script>
import { auth } from '../lib/firebase-client';
import { signInWithEmailAndPassword } from 'firebase/auth';
const form = document.getElementById('login-form');
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 idToken = await userCredential.user.getIdToken();
const response = await fetch('/api/auth/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ idToken }),
});
if (response.ok) {
window.location.href = '/dashboard';
}
} catch (error) {
console.error('Login failed:', error);
}
});
</script>
</body>
</html>
Session Creation API Route
Convert the Firebase ID token into a session cookie. Endpoints render on demand the same way pages do, so the route exports prerender = false. The cookie is named __session because that is the only cookie name Firebase Hosting forwards to a backend, and it is the name the official Astro Firebase guide uses:
// src/pages/api/auth/session.ts
import type { APIRoute } from 'astro';
import { adminAuth } from '../../../lib/firebase-admin';
export const prerender = false;
export const POST: APIRoute = async ({ request }) => {
const { idToken } = await request.json();
try {
// Firebase allows session cookies from 5 minutes to 2 weeks.
const expiresIn = 60 * 60 * 24 * 7 * 1000; // 7 days, within the 14-day max
const sessionCookie = await adminAuth.createSessionCookie(idToken, { expiresIn });
return new Response(JSON.stringify({ status: 'success' }), {
headers: {
'Set-Cookie': `__session=${sessionCookie}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=${expiresIn / 1000}`,
'Content-Type': 'application/json',
},
});
} catch (error) {
return new Response(JSON.stringify({ error: 'Invalid token' }), { status: 401 });
}
};
Authentication Middleware
Verify the session on every request:
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
import { adminAuth } from './lib/firebase-admin';
export const onRequest = defineMiddleware(async ({ request, locals, cookies }, next) => {
const sessionCookie = cookies.get('__session')?.value;
if (sessionCookie) {
try {
// The second argument is checkRevoked: true runs an extra check that
// detects revoked sessions, deleted users, and disabled accounts.
const decodedClaims = await adminAuth.verifySessionCookie(sessionCookie, true);
locals.user = decodedClaims;
} catch {
locals.user = null;
}
} else {
locals.user = null;
}
return next();
});
Middleware always runs on the server, so it does not need a prerender export. It runs on every matching request, including ones for statically prerendered pages, which is why guarding access in the page frontmatter (below) still matters.
Protecting Pages
---
// src/pages/dashboard.astro
export const prerender = false;
const user = Astro.locals.user;
if (!user) {
return Astro.redirect('/login');
}
---
<html>
<body>
<h1>Dashboard</h1>
<p>Welcome, {user.email}</p>
<p>Firebase UID: {user.uid}</p>
</body>
</html>
Troubleshooting
"Firebase app already initialized" error: This happens during hot reload in development. The getApps() check in the admin setup file prevents it. Make sure you are using the pattern shown above rather than calling initializeApp directly.
Private key parsing fails: The FIREBASE_ADMIN_PRIVATE_KEY often has escaped newlines when stored in .env files. The .replace(/\\n/g, '\n') in the config handles this. If deploying to a platform like Vercel, paste the key without escaping and let the platform handle it.
Session cookie not created: For sensitive flows Firebase recommends only minting a session cookie when the user signed in recently. Its session-cookie docs check auth_time and process the request only if the user "just signed in in the last 5 minutes," forcing a fresh re-authentication otherwise. The createSessionCookie call itself also fails on an invalid or already-expired ID token, so call it right after getIdToken().
CORS errors with Google sign-in: When using social providers, add your domain (including localhost with port) to Firebase Console > Authentication > Settings > Authorized domains.
Session expiration handling: Firebase session cookies accept an expiresIn between 5 minutes and 2 weeks (14 days); values outside that range are rejected. When a session expires, the middleware sets locals.user to null and the page redirect sends the user back to login.
Common Errors and Fixes
output: 'hybrid' is not a valid config: If you are following an older tutorial that sets output: 'hybrid' in astro.config.mjs, remove it. That mode was removed in Astro 5, and Astro 6 keeps static as the default. Mark individual server-rendered files with export const prerender = false instead, as the on-demand rendering guide documents.
Pages render at build time instead of per request: If a login or dashboard page returns stale or empty Astro.locals, you almost certainly forgot export const prerender = false on that file, or you never added an adapter. On-demand rendering requires both. Without an adapter, Astro has no server runtime to render the route at request time.
Cannot read properties of undefined from getApps() / duplicate-app error: The Admin SDK is modular as of v10 and stays modular in v13. Import initializeApp, cert, and getApps from firebase-admin/app and getAuth from firebase-admin/auth. Mixing the legacy import admin from 'firebase-admin' namespace with the modular entry points, or calling initializeApp twice without the getApps() guard, throws an "app already exists" error.
Admin credentials read as undefined on the server: import.meta.env only exposes non-PUBLIC_ variables to server-side code, never to the client bundle. Confirm the file using the admin credentials runs on the server (an endpoint, middleware, or a prerender = false page), not inside a <script> tag or a client:* component. For a schema-validated alternative, declare the keys with astro:env and read them via getSecret() from astro:env/server.
Private key newline error (Failed to parse private key): When the service-account private key lives in a .env file, its newlines are escaped as literal \n. The FIREBASE_ADMIN_PRIVATE_KEY?.replace(/\\n/g, '\n') in the admin setup restores them. On hosts that store the value verbatim (some inject real newlines), the replace is a no-op and stays safe.
Cookie ignored behind Firebase Hosting: Firebase Hosting only forwards a cookie named __session to a backend. Using any other name (such as session) means the cookie never reaches your server function. This guide uses __session throughout, matching the official Astro Firebase guide.
Node version errors during npm install or build: Astro 6 declares node >=22.12.0 in its engines field. On Node 18 or 20 you will see engine warnings and may hit build failures. Upgrade Node before installing.
Official Docs and Examples
- Astro official Firebase backend guide (client setup, Admin SDK with
getApps()andcert(),createSessionCookie,verifySessionCookie, the__sessioncookie, and OAuth providers): https://docs.astro.build/en/guides/backend/firebase/ - Astro on-demand rendering guide (default static output,
export const prerender = false, adapters): https://docs.astro.build/en/guides/on-demand-rendering/ @astrojs/nodeadapter reference (install command,standalonevsmiddlewaremodes, config): https://docs.astro.build/en/guides/integrations-guide/node/- Astro environment variables guide and the
astro:envAPI reference: https://docs.astro.build/en/guides/environment-variables/ and https://docs.astro.build/en/reference/modules/astro-env/ - Firebase manage session cookies (createSessionCookie, verifySessionCookie,
expiresIn5-minute-to-2-week range, recent sign-in check): https://firebase.google.com/docs/auth/admin/manage-cookies - Firebase Admin SDK setup: https://firebase.google.com/docs/admin/setup
- Firebase Auth web quickstart: https://firebase.google.com/docs/auth/web/start
- Astro official SSR example repo: https://github.com/withastro/astro/tree/main/examples/ssr
Conclusion
Firebase Auth with Astro uses a client-server split where Firebase handles the initial authentication on the client, and the Admin SDK verifies sessions server-side. Session cookies give you secure, HttpOnly tokens that work with server-rendered pages. On Astro 6 you keep the default static output and opt specific pages and endpoints into on-demand rendering with export const prerender = false plus an adapter. This approach keeps your authentication flow standard and secure while taking advantage of Firebase's built-in support for social providers, multi-factor authentication, and user management.
Sources
- Astro on-demand rendering guide, checked on 2026-05-29: https://docs.astro.build/en/guides/on-demand-rendering/
@astrojs/nodeadapter reference, checked on 2026-05-29: https://docs.astro.build/en/guides/integrations-guide/node/- Astro using environment variables guide, checked on 2026-05-29: https://docs.astro.build/en/guides/environment-variables/
- Astro
astro:envAPI reference, checked on 2026-05-29: https://docs.astro.build/en/reference/modules/astro-env/ - Astro official Firebase backend guide, checked on 2026-05-29: https://docs.astro.build/en/guides/backend/firebase/
- Astro SSR example, checked on 2026-05-29: https://github.com/withastro/astro/tree/main/examples/ssr
- Firebase manage session cookies, checked on 2026-05-29: https://firebase.google.com/docs/auth/admin/manage-cookies
- Firebase Admin SDK setup, checked on 2026-05-29: https://firebase.google.com/docs/admin/setup
- Firebase Auth web quickstart (signInWithEmailAndPassword, getIdToken), checked on 2026-05-29: https://firebase.google.com/docs/auth/web/start
- npm registry,
firebaselatest = 12.14.0, checked on 2026-05-29: https://registry.npmjs.org/firebase/latest - npm registry,
firebase-adminlatest = 13.10.0 (engines.node >=18), checked on 2026-05-29: https://registry.npmjs.org/firebase-admin/latest - npm registry,
astrolatest = 6.4.2 (engines.node >=22.12.0), checked on 2026-05-29: https://registry.npmjs.org/astro/latest - npm registry,
@astrojs/nodelatest = 10.1.2, checked on 2026-05-29: https://registry.npmjs.org/@astrojs/node/latest
Related Articles
How to Use Algolia with Astro: Complete Guide
Step-by-step guide to integrating Algolia with your Astro website.
How to Integrate Auth0 with Astro: Complete Guide
Step-by-step guide to integrating Auth0 with your Astro website. Setup, configuration, and best practices.
How to Use AWS Amplify with Astro: Complete Guide
Step-by-step guide to integrating AWS Amplify with your Astro website.