Firebase Auth und Firestore in Next.js ohne flache Sammlungen
Schützen Sie eine einzige Firebase-App-Instanz, lassen Sie die Auth-Hilfsfunktionen typisierte Fehler auslösen, ordnen Sie Übungen unter jedem Benutzer an und überprüfen Sie Schreibvorgänge mit den Emulatoren.
Übernommene Firebase- und Next.js-Startprojekte weisen oft zwei Probleme auf: eine einfache workouts-Sammlung, die alle Dokumente eines Kontos enthält, sowie Catch-Blöcke, die Fehler protokollieren und anschließend undefined zurückgeben, sodass die Aufrufer nie erfahren, dass eine Schreiboperation fehlgeschlagen ist. Demos mit einem einzigen Anmeldevorgang verbergen beide Probleme – im Produktbetrieb jedoch nicht.
Der Kern für Authentifizierung und Tracking in einer Fitness-App wurde mit dem Firebase JS SDK 12.17.1 neu aufgebaut und an den Firebase-Emulatoren getestet, um Schreibvorgänge von Anfang bis Ende überprüfen zu können. Die unten stehenden Notizen konzentrieren sich auf die frühzeitige Behebung der Strukturprobleme.
Eine Firebase-Instanz, nicht fünf
Ein häufiges Problem ist die Aufrufung von initializeApp ganz oben in einem Modul, das von mehreren Routen importiert wird. Next.js’ Hot-Reload-Funktion sowie die Trennung der Server- und Client-Bundles können dieses Modul zweimal laden; beim zweiten Aufruf wird dann angezeigt, dass bereits eine Standardanwendung existiert. Schützen Sie sich davor:
import { initializeApp, getApps, getApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
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!,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET!,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_SENDER_ID!,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID!,
};const app = getApps().length ? getApp() : initializeApp(firebaseConfig);export const auth = getAuth(app);
export const db = getFirestore(app);
getApps().length ? getApp() : initializeApp(...) ist die gesamte Schutzmaßnahme. Legen Sie die Konfiguration in die NEXT_PUBLIC_-Umgebungsvariablen statt durch Hardcoding fest – nicht, weil die Firebase-Konfiguration für das Web geheim ist (sie wird per Design zum Browser gesendet), sondern weil getrennte Projekte für Entwicklung und Produktion nicht dazu führen sollten, dass man den Quellcode ändern muss, um zwischen ihnen zu wechseln.
Ältere Tutorials fügen Zeichenketten wie "your-api-key" in die Datei ein. Das ist an sich kein Sicherheitsproblem; es handelt sich dabei um eine Gewohnheit, die letztendlich dazu führt, dass echte Produktionskonfigurationen in öffentlichen Repositorien landen.
Authentifizierung, die etwas zurückgibt, das der Aufrufer verwenden kann
Verwenden Sie diese Struktur. Beachten Sie, was sie nicht tut: Fehler abfangen und undefined zurückgeben.
import {
createUserWithEmailAndPassword,
type User,
} from "firebase/auth";
import { addDoc, collection, serverTimestamp } from "firebase/firestore";
import { auth, db } from "./firebase";
export async function registerUser(
email: string,
password: string,
): Promise<User> {
const cred = await createUserWithEmailAndPassword(auth, email, password);
return cred.user;
}
Ältere Hilfsfunktionen umhüllen den Aufruf mit try/catch, geben einen Registrierungsfehler aus und liefern undefined zurück. Der UI-Code wartet anschließend auf registerUser(...) und versucht, auf user.uid zuzugreifen – bei einem möglicherweise fehlenden Wert. Dadurch wird ein falsches Passwort oder eine doppelte E-Mail niemals als klarer Authentifizierungsfehler erkannt; stattdessen tritt das Problem später beim Lesen einer Eigenschaft von undefined auf, weit entfernt vom eigentlichen Fehler.
Besser ist die Ablehnung des Vorgangs. Der Firebase-Authentifizierungsaufruf löst typisierte Fehler aus (auth/email-already-in-use, auth/weak-password und ähnliche). Weisen Sie diese Codes in der Anzeige in verständliche Begriffe um. Sorgen Sie dafür, dass die Datenhilfsfunktion ehrlich ist: Entweder erfolgreich oder mit einem Fehleraufruf.
Übungen bereits ab dem ersten Tag nach Benutzer strukturieren
Diese Änderung ist besonders wichtig, wenn es um etwas weiter als ein Prototyp geht. Veraltete Beispiele erstellen eine einfache workouts-Sammlung mit einem userId-Feld:
// what everyone copies — one collection for the whole app
addDoc(collection(db, "workouts"), { userId, ...workout });
Die Abfrage „meine Workouts“ durchsucht eine Sammlung, die mit der gesamten Nutzerbasis wächst, und Sicherheitsregeln müssen für jede Operation nach dem userId filtern. Es ist besser, eine Unter Sammlung zu verwenden, sodass die Workouts jedes Benutzers in seinem eigenen Dokument gespeichert sind:
export type WorkoutInput = {
type: string;
durationMinutes: number;
caloriesBurned: number;
};
export async function addWorkoutSession(
userId: string,
workout: WorkoutInput,
): Promise<string> {
const ref = await addDoc(collection(db, "users", userId, "workouts"), {
...workout,
createdAt: serverTimestamp(),
});
return ref.id;
}
collection(db, „users“, userId, „workouts“) zielt auf die private Unter Sammlung des jeweiligen Kontos ab. Sicherheitsregeln können anschließend request.auth.uid mit dem {uid}-Pfadsegment sowohl bei Lese- als auch Schreibvorgängen vergleichen, sodass jede Abfrage innerhalb der Dokumente eines einzelnen Benutzers bleibt.
Zwei Details, die hervorgehoben werden sollten. Verwenden Sie lieber serverTimestamp() statt new Date(): Client-Uhren (oder bösartige Clients) speichern fehlerhafte Zeitstempel; serverTimestamp() ist ein Sicherheitsmechanismus, den Firestore beim Speichern mit der Serverzeit füllt und der nicht gefälscht werden kann. Wenn der Payload als WorkoutInput und nicht als any definiert wird, werden Tippfehler in den Feldnamen erkannt, die sonst erst dann sichtbar werden, wenn ein Diagramm stillschweigend nichts anzeigt.
Beweisen, dass tatsächlich geschrieben wird
Vertrauen Sie nicht auf Firebase-Code, der noch nie im Emulator getestet wurde – das SDK akzeptiert Aufrufe, die eine echte Sicherheitsregel ablehnen würde. Richten Sie das SDK auf lokale Emulatoren aus und führen Sie den gesamten Ablauf durch: Registrierung, Schreiben und Zurücklesen.
import { getAuth, connectAuthEmulator, createUserWithEmailAndPassword } from "firebase/auth";
import { getFirestore, connectFirestoreEmulator, addDoc, getDocs, collection, serverTimestamp } from "firebase/firestore";
connectAuthEmulator(auth, "http://127.0.0.1:9099", { disableWarnings: true });
connectFirestoreEmulator(db, "127.0.0.1", 8080);const cred = await createUserWithEmailAndPassword(auth, email, "s3cret-pass");
const ref = await addDoc(
collection(db, "users", cred.user.uid, "workouts"),
{ type: "run", durationMinutes: 32, caloriesBurned: 410, createdAt: serverTimestamp() },
);
const snap = await getDocs(collection(db, "users", cred.user.uid, "workouts"));
Durch Ausführung derselben Ablauffolge mit dem Emulator-Runner erhielt man einen konkreten Auth-UID, eine Schreib-ID sowie ein Dokument zum Zurücklesen, dessen createdAt ein echter Firestore-Zeitstempel (Seconds/Nanoseconds) war und nicht der Standardwert – was beweist, dass der Server das Feld ausgefüllt hat. Falsche Pfade oder Feldtypen führen auf dem Laptop zum Scheitern, nicht nach der Bereitstellung.
Hinweis zu den Tools: Die Firebase CLI erfordert nun Java 21 oder neuer. Ein älteres JRE auf einem sauberen Laptop verhindert mit einem Versionsfehler den Start des Emulator-Runners, bevor irgendein App-Code ausgeführt wird. Erstellen Sie ein neueres JDK und versuchen Sie es erneut.
Welcher nächste Schritt?
Authentifizierung zusammen mit einer korrekt abgegrenzten Schreibzugriffsmöglichkeit bilden die Grundlage. Ein fertiges Produkt verbindet in der Regel onAuthStateChanged mit dem Zustand der angemeldeten Benutzeroberfläche, übersetzt Authentifizierungsfehlercodes in eine übersichtliche Form und listet die Historie mithilfe von getDocs, orderBy("createdAt", "desc") sowie einem limit auf. All diese Funktionen beruhen weiterhin auf zwei früh getroffenen Entscheidungen: Firebase einmal hinter einer Schutzmaßnahme initialisieren und die Trainingsdokumente unter dem jeweiligen Benutzer ablegen. Fehlt eine dieser Entscheidungen, leiden spätere Funktionen darunter.