Fixing document is not defined when importing firebase auth in Next App Router

Fixing “document is not defined” when importing firebase/auth in Next.js (App Router)

We may earn an affiliate commission through purchases made from our guides and tutorials.

You’re seeing this error because the Firebase Web SDK is a browser library and Next.js does server-side rendering by default. When Node.js renders your component, document doesn’t exist—so importing or running firebase/auth on the server triggers ReferenceError: document is not defined. You’ll fix it by isolating all Firebase client imports to Client Components (or to effects that only run in the browser) and using the Admin SDK on the server when needed.

Below you’ll find a complete, modern setup for Next.js App Router (13.4+) and Firebase JS SDK v11+ with precise guardrails to keep browser-only code out of SSR. The pattern scales from local dev to production without hacks. First I’ll show the minimal, reliable client-only import. Then I’ll cover common pitfalls, SSR/Edge gotchas, server-side alternatives, and a final checklist.

The root cause in one line

Next.js renders routes on the server unless a component is explicitly a Client Component. Importing firebase/auth (or anything that touches browser persistence) in a server context leads to document errors. Mark client code with "use client" and never import firebase/auth from a server file.

Gold-standard setup (App Router)

This structure avoids SSR pitfalls and duplicate initialization while keeping imports clean.

app/firebase/client.ts — browser-only Firebase client

"use client";

import { initializeApp, getApps, getApp, type FirebaseApp } from "firebase/app";
import {
  getAuth,
  setPersistence,
  browserLocalPersistence,
  type Auth,
} from "firebase/auth";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID!,
};

let app: FirebaseApp;
if (!getApps().length) {
  app = initializeApp(firebaseConfig);
} else {
  app = getApp();
}

/**
 * Export `auth` only on the client. On the server, never import this file.
 * This prevents `document`/`window` access during SSR.
 */
export const auth: Auth = getAuth(app);

// Optional: choose persistence explicitly to avoid implicit DOM access during boot.
setPersistence(auth, browserLocalPersistence).catch(() => {
  // No-op on hydration race; you may log if desired.
});

app/(auth)/login/LoginForm.tsx — client component that uses Auth

"use client";

import { useState } from "react";
import { auth } from "@/app/firebase/client";
import { signInWithEmailAndPassword } from "firebase/auth";

export default function LoginForm() {
  const [email, setEmail] = useState("");
  const [pw, setPw] = useState("");
  const [err, setErr] = useState<string | null>(null);

  const onSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setErr(null);
    try {
      await signInWithEmailAndPassword(auth, email, pw);
      // redirect or refresh as needed
    } catch (error: any) {
      setErr(error?.message ?? "Login failed");
    }
  };

  return (
    <form onSubmit={onSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input value={pw} type="password" onChange={(e) => setPw(e.target.value)} />
      <button type="submit">Sign in</button>
      {err && <p role="alert">{err}</p>}
    </form>
  );
}

app/(auth)/login/page.tsx — server component that renders a client child

// This is a Server Component by default.
// It does NOT import firebase/auth or the client module.
import LoginForm from "./LoginForm";

export default function Page() {
  return <LoginForm />;
}

This arrangement ensures server code never imports firebase/auth, while client code does so safely under "use client". It fixes the document error and aligns with Next’s intended split of server vs. client responsibilities.

Quick alternatives when you cannot move a file to client

Sometimes a third-party widget or a legacy file still trips SSR. These patterns are safe stopgaps:

Dynamic import with SSR disabled (component-level):

import dynamic from "next/dynamic";

const ClientOnlyLoginForm = dynamic(() => import("./LoginForm"), { ssr: false });

export default function Page() {
  return <ClientOnlyLoginForm />;
}

This defers rendering to the browser so document exists when the component loads. Prefer the “Gold-standard setup” above for most pages; use ssr: false sparingly for problematic libraries.

Guard inside effects for one-off DOM usage:

"use client";
import { useEffect } from "react";

useEffect(() => {
  // Safe: runs only in the browser after mount
  document.title = "Welcome";
}, []);

Effects run only on the client; keep any document or window usage here.

What not to do (and why)

  1. Do not import firebase/auth in a Server Component, route handler, or Server Action. Even if code “looks” unused, the module’s side effects (persistence, browser globals) can touch document. Move those imports to Client Components, or inject them with a dynamic import as shown above.
  2. Do not try to use the Web SDK for server logic. Use the Admin SDK (firebase-admin) on the server (Route Handlers, Server Actions, or background jobs). The Admin SDK is designed for Node.js and has no DOM assumptions.

Correct server-side usage (Admin SDK)

If you need to verify ID tokens, read/write protected Firestore documents, or manage users on the server, initialize Admin in a server-only module. Never import the client module here.

app/firebase/admin.ts — server-only

// Server file: do NOT add "use client"
import { cert, getApps, initializeApp, getApp, type App } from "firebase-admin/app";
import { getAuth } from "firebase-admin/auth";

const firebaseAdminApp: App =
  getApps().length
    ? getApp()
    : initializeApp({
        credential: cert({
          projectId: process.env.FIREBASE_PROJECT_ID!,
          clientEmail: process.env.FIREBASE_CLIENT_EMAIL!,
          privateKey: process.env.FIREBASE_PRIVATE_KEY!.replace(/\\n/g, "\n"),
        }),
    });

export const adminAuth = getAuth(firebaseAdminApp);

Use adminAuth.verifyIdToken() in Route Handlers or Server Actions to gate server data. This keeps the Web SDK out of SSR entirely.

Edge Runtime and deployment notes

If you target the Edge Runtime for Route Handlers or Server Actions, avoid any Node-only modules (Admin SDK requires Node APIs). Use runtime = "nodejs" for endpoints that use firebase-admin, or keep Admin work in traditional Node routes while using Edge elsewhere. This separation prevents runtime import errors and still lets you benefit from Edge where appropriate.

Common causes of document is not defined with Firebase—and the fix

  • Importing firebase/auth from a server file (e.g., app/page.tsx that’s a Server Component).
    Fix: move Firebase imports into a Client Component ("use client" at top) or wrap with dynamic(..., { ssr: false }).
  • Creating a shared firebase.ts used by both server and client.
    Fix: split into two files: firebase/client.ts (Web SDK) and firebase/admin.ts (Admin SDK). Server files must not import the client one.
  • Using browser persistence implicitly during SSR.
    Fix: initialize Auth only in client code and set explicit persistence (as shown earlier), or guard with typeof window !== "undefined".
  • Calling Firebase Client SDK inside Server Actions or Route Handlers.
    Fix: move to a Client Component or switch to Admin SDK on the server. Some environments note specific incompatibilities when firebase/auth leaks into server contexts.

Minimal reproduction to confirm the fix

Create a fresh Next.js App Router project, add Firebase v11+, and drop in only the client module and client component above. If you can navigate and reload the login page without errors, your import boundaries are correct. If you reintroduce the error after adding new code, search for a stray server import of firebase/auth or a third-party package that reads document during SSR. General Next.js “document/window is not defined” patterns also apply here.

Troubleshooting checklist

  • The file that imports firebase/auth starts with "use client".
  • No server file (Route Handler, Server Component, middleware, Server Action) imports the client module.
  • Admin-only logic uses firebase-admin with runtime = "nodejs" where required.
  • Suspicious third-party UI libraries are either wrapped with dynamic(..., { ssr: false }) or loaded inside useEffect.
  • On reload and production build, the page renders without document errors.

If any box is unchecked, you’ll likely see the error again.

Where this leaves you

You’ve separated browser-only Firebase Auth from server rendering, so Next.js can SSR safely while your client code uses the Web SDK with no document errors. From here, layer on features—protected routes, token verification with Admin on the server, and a deployment to DigitalOcean App Platform or your host of choice—without changing the import patterns you established today. If you later adopt Edge for select routes, keep Admin work on Node only to avoid runtime quirks.

Notes on versions and docs used while validating this approach

  • The error arises from SSR accessing browser globals; fixes rely on Client Components, dynamic import, or effect scoping.
  • Keep Firebase Web SDK (client) and Admin SDK (server) separate; use each in its intended environment.

If you follow the “Gold-standard setup” exactly, the “document is not defined” error will disappear and stay gone.

Was this helpful?

Thanks for your feedback!
Alex is the resident editor and oversees all of the guides published. His past work and experience include Colorlib, Stack Diary, Hostvix, and working with a number of editorial publications. He has been wrangling code and publishing his findings about it since the early 2000s.

Leave a comment

Your email address will not be published. Required fields are marked *