
I was building a Next.js app with Firebase Authentication (email/password), and 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. So I started researching it and found that this issue is still not resolved. People have created GitHub issues and have been discussing it on Reddit for a long time.
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.
Here’s what my original code looked like:
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);
}
};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 reasons
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);
}
};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);const userProfile = {
uid: user.uid, //****// Still keep UID for reference
email: user.email,
userType: "staff",
createdAt: serverTimestamp(),
updatedAt: serverTimestamp(),
};✅ 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", "[email protected]"));
```
**Still have UID**: Stored as a field for any UID-based operations
## Firestore Structure
Your Firestore `users` collection now looks like this:
```
users/
├── [email protected]/
│ ├── uid: "abc123xyz"
│ ├── email: "[email protected]"
│ ├── userType: "staff"
│ ├── createdAt: timestamp
│ └── updatedAt: timestamp
│
└── [email protected]/
├── uid: "def456uvw"
├── email: "[email protected]"
└── ...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
"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>
);
}2
4
0