While building a Next.js app with Firebase Authentication (email/password), I encountered a frustrating issue — users could sign up multiple times with the same email address, creating duplicate entries in my Firestore database.
Even though Firebase Auth is supposed to prevent duplicate emails automatically, I was still seeing duplicates. After digging through GitHub issues and Reddit discussions, I realized this problem has been around for a while.
After spending an entire day, I finally managed to resolve the issue in a tricky way. All you have to do is use Firestore Database for this.
My Original (Problematic) Code
const createUserProfile = async (user: any) => {
try {
// **** // Problem: Using UID as document ID
const userDocRef = doc(db, "users", user.uid);
const userProfile = {
uid: user.uid,
email: user.email,
userType: "staff",
createdAt: serverTimestamp(),
updatedAt: serverTimestamp(),
};
await setDoc(userDocRef, userProfile);
console.log("User profile created");
} catch (error) {
console.error("Error creating user profile:", error);
throw error;
}
};
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError("");
try {
// ***** // No check before creating account
const userCredential = await createUserWithEmailAndPassword(
auth,
email,
password
);
await createUserProfile(userCredential.user);
router.push("/dashboard");
} catch (error: any) {
if (error.code === "auth/email-already-in-use") {
setError("Email already registered.");
}
} finally {
setLoading(false);
}
};
Why This Approach Failed
The problem with this common approach:
- No pre-check: We only found out the email was taken AFTER trying to create the Firebase Auth account
- **UID **as document ID: We used user.uid as the Firestore document ID, but we don't know the UID until AFTER the user is created
- Can’t check by email: With UID as the document ID, we couldn’t easily check if an email already exists in Firestore
- fetchSignInMethodsForEmail() doesn't work: This Firebase method is often disabled due to "Email Enumeration Protection" for security reason s
The Solution: Use Email as Document ID
The key insight: Use the email address as the Firestore document ID instead of the UID. This way, we can check if an email exists BEFORE attempting to create the Firebase Auth account.
const createUserProfile = async (user: any) => {
try {
//****// Use EMAIL as document ID instead of UID
const userDocRef = doc(db, "users", user.email);
const userDoc = await getDoc(userDocRef);
if (!userDoc.exists()) {
const userProfile = {
uid: user.uid,
email: user.email,
userType: "staff",
createdAt: serverTimestamp(),
updatedAt: serverTimestamp(),
};
await setDoc(userDocRef, userProfile);
} else {
console.log("User profile already exists");
}
} catch (error) {
console.error("Error creating user profile:", error);
throw error;
}
};
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError("");
try {
//****// Check if email exists in Firestore BEFORE creating auth account
const userDocRef = doc(db, "users", email);
const userDoc = await getDoc(userDocRef);
if (userDoc.exists()) {
setError("You already have an account with this email. Please sign in instead.");
setLoading(false);
return;
}
//****// Create Firebase Auth account
const userCredential = await createUserWithEmailAndPassword(
auth,
email,
password
);
//****// Create Firestore profile
await createUserProfile(userCredential.user);
router.push("/dashboard");
} catch (error: any) {
if (error.code === "auth/email-already-in-use") {
setError("You already have an account with this email. Please sign in instead.");
} else if (error.code === "auth/weak-password") {
setError("Password should be at least 6 characters.");
} else if (error.code === "auth/invalid-email") {
setError("Invalid email address.");
} else {
setError(error.message);
}
} finally {
setLoading(false);
}
};
**
Key Changes Explained
**
1. Changed Document ID from UID to Email
Before:
//***// Can't check email existence before signup
const userDocRef = doc(db, "users", user.uid);
After:
//***// Can check email existence anytime
const userDocRef = doc(db, "users", user.email);
2. Added Pre-Check Before Creating Account
Before:
//***// No check - try to create and handle error later
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
After:
//***// Check Firestore first
const userDocRef = doc(db, "users", email);
const userDoc = await getDoc(userDocRef);
if (userDoc.exists()) {
setError("You already have an account with this email. Please sign in instead.");
return;
}
//***// Only create account if email doesn't exist
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
3. Store UID as a Field
const userProfile = {
uid: user.uid, // ✅ Still keep UID for reference
email: user.email,
userType: "staff",
createdAt: serverTimestamp(),
updatedAt: serverTimestamp(),
};
** ## Why This Solution Works **
✅ Benefits:
- Fast email lookup: Direct document access instead of querying
// Super fast O(1) lookup
const userDoc = await getDoc(doc(db, "users", email));
- Prevents duplicates: Checks BEFORE attempting Firebase Auth signup
- Better UX: Users get immediate feedback without waiting for Firebase Auth error
- No enumeration protection issues: Doesn’t rely on fetchSignInMethodsForEmail()
- Easy queries: Finding users by email is now trivial
//****// Get user by email
const user = await getDoc(doc(db, "users", "user@example.com"));
Still have UID: Stored as a field for any UID-based operations
Firestore Structure
Your Firestore users
collection now looks like this:
users/
├── user@example.com/
│ ├── uid: "abc123xyz"
│ ├── email: "user@example.com"
│ ├── userType: "staff"
│ ├── createdAt: timestamp
│ └── updatedAt: timestamp
│
└── another@example.com/
├── uid: "def456uvw"
├── email: "another@example.com"
└── ...
**
What About Existing Users?
**
If you already have users with UID as document ID, you have a few options:
//***// One-time migration script
const migrateUsers = async () => {
const usersRef = collection(db, "users");
const snapshot = await getDocs(usersRef);
for (const docSnap of snapshot.docs) {
const userData = docSnap.data();
//***// Create new document with email as ID
await setDoc(doc(db, "users", userData.email), userData);
//***// Optional: Delete old document
//***// await deleteDoc(doc(db, "users", docSnap.id));
}
};
NOTE: If you’re in development, delete old users and use the new structure
Here’s the Whole Sign-up Code:
"use client";
import React, { useState } from "react";
import { createUserWithEmailAndPassword } from "firebase/auth";
import { doc, setDoc, getDoc, serverTimestamp } from "firebase/firestore";
import { auth, db } from "@/firebase/client";
import { useRouter } from "next/navigation";
import { Input } from "../ui/input";
import Link from "next/link";
export default function SignUpForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const createUserProfile = async (user: any) => {
try {
//***// Use EMAIL as document ID instead of UID
const userDocRef = doc(db, "users", user.email);
const userDoc = await getDoc(userDocRef);
if (!userDoc.exists()) {
const userProfile = {
uid: user.uid, // Store UID as a field for reference
email: user.email,
userType: "staff",
createdAt: serverTimestamp(),
updatedAt: serverTimestamp(),
};
await setDoc(userDocRef, userProfile);
console.log("User profile created successfully");
} else {
console.log("User profile already exists");
}
} catch (error) {
console.error("Error creating user profile:", error);
throw error;
}
};
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError("");
try {
//***// Check if email exists in Firestore BEFORE creating auth account
const userDocRef = doc(db, "users", email);
const userDoc = await getDoc(userDocRef);
if (userDoc.exists()) {
setError("You already have an account with this email. Please sign in instead.");
setLoading(false);
return;
}
//***// Create Firebase Auth account
const userCredential = await createUserWithEmailAndPassword(
auth,
email,
password
);
//***// Create Firestore profile
await createUserProfile(userCredential.user);
router.push("/dashboard");
} catch (error: any) {
if (error.code === "auth/email-already-in-use") {
setError("You already have an account with this email. Please sign in instead.");
} else if (error.code === "auth/weak-password") {
setError("Password should be at least 6 characters.");
} else if (error.code === "auth/invalid-email") {
setError("Invalid email address.");
} else {
setError(error.message);
}
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-neutral-900 py-12 px-4">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-medium capitalize text-gray-100">
Create your account
</h2>
<p className="mt-2 text-center text-4xl text-gray-400">
Welcome to Inspire
</p>
</div>
<div className="mt-8 space-y-6">
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}
<div className="space-y-4">
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-400"
>
Email address
</label>
<Input
id="email"
name="email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 appearance-none relative block w-full h-12 border"
placeholder="Email address"
/>
</div>
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-400"
>
Password
</label>
<Input
id="password"
name="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 appearance-none relative block w-full h-12 border"
placeholder="Password (min 6 characters)"
/>
</div>
</div>
<div>
<button
type="button"
onClick={handleSignUp}
disabled={loading}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
{loading ? "Creating account..." : "Sign Up"}
</button>
</div>
<div className="text-center">
<Link
href="/signin"
className="text-indigo-600 hover:text-indigo-500 text-sm"
>
Already have an account? Sign in
</Link>
</div>
</div>
</div>
</div>
);
}
Top comments (0)