Skip to main content
RapidDev - Software Development Agency
firebase-tutorial

How to Prevent Duplicate Documents in Firestore

Firestore does not enforce unique constraints on fields. To prevent duplicate documents, use deterministic document IDs derived from the unique value (like an email or username) with setDoc so a second write overwrites instead of creating a new document. For conditional creates — only create if the document does not exist — use a transaction that reads first and writes only if the document is absent. Security rules can also reject creates when a document already exists by checking resource == null.

What you'll learn

  • How to use deterministic document IDs to enforce uniqueness
  • How to use transactions for conditional document creation
  • How to write security rules that prevent duplicate creates
  • How to implement a uniqueness lookup collection pattern
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate8 min read12-15 minFirebase JS SDK v9+, Firestore (all plans)March 2026RapidDev Engineering Team
TL;DR

Firestore does not enforce unique constraints on fields. To prevent duplicate documents, use deterministic document IDs derived from the unique value (like an email or username) with setDoc so a second write overwrites instead of creating a new document. For conditional creates — only create if the document does not exist — use a transaction that reads first and writes only if the document is absent. Security rules can also reject creates when a document already exists by checking resource == null.

Preventing Duplicate Documents in Firestore

Unlike SQL databases with UNIQUE constraints, Firestore has no built-in way to enforce field uniqueness. If two clients call addDoc at the same time with the same email, you get two documents. This tutorial covers three strategies to prevent duplicates: deterministic IDs, transactions for conditional creates, and a lookup collection pattern for enforcing uniqueness across any field.

Prerequisites

  • A Firebase project with Firestore enabled
  • Firebase JS SDK v9+ installed
  • Basic understanding of Firestore document IDs and setDoc vs addDoc
  • Familiarity with Firestore transactions and security rules

Step-by-step guide

1

Use deterministic document IDs for natural uniqueness

The simplest way to prevent duplicates is to use the unique value itself as the document ID. Instead of addDoc (which generates a random ID), use setDoc with doc(db, 'collection', uniqueValue). If two clients write the same email, they both write to the same document path, so only one document exists. This works well when you have a clear unique field like email, username, or external ID.

typescript
1import { doc, setDoc, serverTimestamp } from 'firebase/firestore'
2
3// Use email as document ID — guarantees one doc per email
4async function createUserProfile(email: string, displayName: string) {
5 const userRef = doc(db, 'user_profiles', email)
6
7 await setDoc(userRef, {
8 email,
9 displayName,
10 createdAt: serverTimestamp(),
11 })
12 // If called twice with the same email, the second call
13 // overwrites the first — no duplicate created
14}
15
16// Use a hash for composite uniqueness
17// e.g., prevent a user from joining the same group twice
18async function joinGroup(userId: string, groupId: string) {
19 const membershipId = `${userId}_${groupId}`
20 const ref = doc(db, 'memberships', membershipId)
21
22 await setDoc(ref, {
23 userId,
24 groupId,
25 joinedAt: serverTimestamp(),
26 })
27}

Expected result: Only one document exists per unique value because the document ID is derived from that value.

2

Use a transaction for conditional create (create-if-not-exists)

When you need to check whether a document exists before creating it — and cannot use the document ID trick — use a transaction. Read the target document inside the transaction. If it does not exist, create it. If it already exists, throw an error or return without writing. The transaction guarantees that no other client can create the same document between your read and write.

typescript
1import { doc, runTransaction, serverTimestamp } from 'firebase/firestore'
2
3async function createUniqueUsername(userId: string, username: string) {
4 const usernameRef = doc(db, 'usernames', username.toLowerCase())
5
6 await runTransaction(db, async (transaction) => {
7 const usernameDoc = await transaction.get(usernameRef)
8
9 if (usernameDoc.exists()) {
10 throw new Error(`Username "${username}" is already taken`)
11 }
12
13 // Create the username reservation
14 transaction.set(usernameRef, {
15 userId,
16 createdAt: serverTimestamp(),
17 })
18
19 // Also update the user's profile with the username
20 transaction.update(doc(db, 'users', userId), {
21 username: username.toLowerCase(),
22 })
23 })
24}

Expected result: The transaction either creates the document (if unique) or throws an error (if duplicate), with no race condition.

3

Enforce uniqueness with security rules

Add a security rule that only allows creates when the document does not already exist. Use the condition resource == null, which is true only for documents that do not exist. Combined with deterministic IDs, this prevents clients from overwriting existing documents. Note that security rules cannot check uniqueness across different documents — they only see the document being written.

typescript
1// firestore.rules
2rules_version = '2';
3service cloud.firestore {
4 match /databases/{database}/documents {
5
6 // Username reservations: only allow create, never overwrite
7 match /usernames/{username} {
8 allow create: if request.auth != null
9 && resource == null // Document must not exist
10 && request.resource.data.userId == request.auth.uid; // Must be their own
11
12 allow read: if true; // Anyone can check availability
13
14 allow delete: if request.auth != null
15 && resource.data.userId == request.auth.uid; // Only owner can delete
16
17 allow update: if false; // Never update, only create/delete
18 }
19 }
20}

Expected result: Security rules block any attempt to create a document that already exists, returning a permission denied error.

4

Implement a lookup collection for field-level uniqueness

When you cannot use the unique field as the document ID (for example, your users collection already uses auto-generated IDs), create a separate lookup collection. The lookup collection uses the unique value as the document ID and stores a reference back to the main document. Check the lookup collection before creating the main document, ideally in a transaction that creates both atomically.

typescript
1import { doc, runTransaction, collection, serverTimestamp } from 'firebase/firestore'
2
3async function createUser(email: string, displayName: string) {
4 // Lookup collection: emails/{email} → { userId }
5 const emailRef = doc(db, 'emails', email.toLowerCase())
6
7 await runTransaction(db, async (transaction) => {
8 // Check if email is already registered
9 const emailDoc = await transaction.get(emailRef)
10 if (emailDoc.exists()) {
11 throw new Error('Email is already registered')
12 }
13
14 // Create the user document with auto-generated ID
15 const userRef = doc(collection(db, 'users'))
16
17 // Create both atomically
18 transaction.set(userRef, {
19 email: email.toLowerCase(),
20 displayName,
21 createdAt: serverTimestamp(),
22 })
23
24 transaction.set(emailRef, {
25 userId: userRef.id,
26 createdAt: serverTimestamp(),
27 })
28 })
29}
30
31// To check availability without creating:
32async function isEmailAvailable(email: string): Promise<boolean> {
33 const emailDoc = await getDoc(doc(db, 'emails', email.toLowerCase()))
34 return !emailDoc.exists()
35}

Expected result: A separate lookup collection enforces uniqueness on any field, with atomic creation of both the main document and the lookup entry.

5

Handle duplicates from offline writes

Firestore caches writes when offline and syncs them when the device reconnects. If two offline devices create documents with the same data, both writes will succeed once they come online (unless you use deterministic IDs). Deterministic IDs are the only reliable prevention for offline scenarios because transactions fail offline. Design your data model so that offline duplicate writes result in idempotent overwrites rather than new documents.

typescript
1import { doc, setDoc, serverTimestamp } from 'firebase/firestore'
2
3// Idempotent write: safe to execute multiple times
4async function submitVote(userId: string, pollId: string, choice: string) {
5 // Deterministic ID: one vote per user per poll
6 const voteId = `${userId}_${pollId}`
7 const voteRef = doc(db, 'votes', voteId)
8
9 // setDoc overwrites if offline duplicate syncs later
10 await setDoc(voteRef, {
11 userId,
12 pollId,
13 choice,
14 votedAt: serverTimestamp(),
15 })
16 // Even if called twice offline, only one vote document exists
17}

Expected result: Deterministic IDs ensure that duplicate offline writes merge into a single document when the device reconnects.

Complete working example

prevent-duplicates.ts
1import { initializeApp } from 'firebase/app'
2import {
3 getFirestore,
4 doc,
5 getDoc,
6 setDoc,
7 runTransaction,
8 collection,
9 serverTimestamp,
10} from 'firebase/firestore'
11
12const app = initializeApp({
13 apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
14 authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,
15 projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,
16})
17const db = getFirestore(app)
18
19// Pattern 1: Deterministic ID
20export async function setProfile(email: string, name: string) {
21 await setDoc(doc(db, 'profiles', email.toLowerCase()), {
22 email: email.toLowerCase(),
23 name,
24 updatedAt: serverTimestamp(),
25 })
26}
27
28// Pattern 2: Transaction-based conditional create
29export async function reserveUsername(userId: string, username: string) {
30 const key = username.toLowerCase()
31 const ref = doc(db, 'usernames', key)
32
33 await runTransaction(db, async (tx) => {
34 const snap = await tx.get(ref)
35 if (snap.exists()) throw new Error('Username taken')
36 tx.set(ref, { userId, createdAt: serverTimestamp() })
37 tx.update(doc(db, 'users', userId), { username: key })
38 })
39}
40
41// Pattern 3: Lookup collection
42export async function createUserWithUniqueEmail(
43 email: string,
44 displayName: string
45) {
46 const emailRef = doc(db, 'emails', email.toLowerCase())
47 await runTransaction(db, async (tx) => {
48 const snap = await tx.get(emailRef)
49 if (snap.exists()) throw new Error('Email already registered')
50 const userRef = doc(collection(db, 'users'))
51 tx.set(userRef, { email: email.toLowerCase(), displayName, createdAt: serverTimestamp() })
52 tx.set(emailRef, { userId: userRef.id, createdAt: serverTimestamp() })
53 })
54}
55
56// Pattern 4: Check availability
57export async function isAvailable(lookupCollection: string, value: string) {
58 const snap = await getDoc(doc(db, lookupCollection, value.toLowerCase()))
59 return !snap.exists()
60}

Common mistakes when preventing Duplicate Documents in Firestore

Why it's a problem: Using addDoc for data that should be unique, resulting in multiple documents with the same value

How to avoid: Use setDoc with a deterministic document ID derived from the unique value. addDoc always creates a new document with a random ID.

Why it's a problem: Checking for existence with getDoc before creating with setDoc outside a transaction, allowing race conditions

How to avoid: Wrap the check-and-create in a runTransaction to guarantee atomicity. Without a transaction, another client can create the document between your read and write.

Why it's a problem: Forgetting to normalize values (lowercase) before using them as keys, treating 'John' and 'john' as different

How to avoid: Always normalize unique values to lowercase (or another canonical form) before using them as document IDs or lookup keys.

Best practices

  • Use deterministic document IDs for natural unique fields like email, username, or external IDs
  • Normalize values to lowercase before using them as uniqueness keys
  • Use transactions for conditional creates when you cannot use the unique field as the document ID
  • Create a separate lookup collection when the main collection uses auto-generated IDs
  • Add security rules with resource == null to enforce server-side uniqueness on deterministic-ID collections
  • Design offline writes to be idempotent using deterministic IDs so duplicates merge on sync
  • Clean up lookup collection entries when the main document is deleted, ideally via a Cloud Function

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I need to prevent duplicate documents in Firestore. Show me three patterns: using deterministic document IDs, using transactions for conditional creates, and using a lookup collection for field-level uniqueness. Use the Firebase modular SDK v9 with TypeScript.

Firebase Prompt

Add username reservation to this Firebase app. When a user picks a username, check if it is available using a Firestore transaction. If available, create a usernames/{username} document and update the user profile. Prevent duplicates with security rules that only allow creates.

Frequently asked questions

Does Firestore have a UNIQUE constraint like SQL?

No. Firestore has no built-in unique constraints on document fields. You must enforce uniqueness at the application level using deterministic IDs, transactions, or lookup collections.

What happens if two clients create the same deterministic ID simultaneously?

Both setDoc calls target the same document path. The last write wins — one overwrites the other. If you need create-only behavior (no overwrites), add a security rule with resource == null or use a transaction.

Can security rules alone prevent duplicates?

Security rules with allow create and resource == null can prevent overwrites on deterministic-ID documents. However, rules cannot check uniqueness across different documents — they only see the document being written.

How do I handle case-sensitive uniqueness?

Always normalize values to lowercase before using them as document IDs or lookup keys. Store the original casing in a separate field if you need to display it. This prevents 'JohnDoe' and 'johndoe' from being treated as different.

Do transactions work for preventing duplicates when the client is offline?

No. Transactions require server connectivity to verify document existence. For offline scenarios, deterministic document IDs are the only reliable duplicate prevention method.

Can RapidDev help implement uniqueness constraints in my Firestore database?

Yes. RapidDev can design and implement the right uniqueness pattern for your data model, including lookup collections, security rules, and Cloud Function triggers to maintain data integrity.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.