# How to Prevent Duplicate Documents in Firestore

- Tool: Firebase
- Difficulty: Intermediate
- Time required: 12-15 min
- Compatibility: Firebase JS SDK v9+, Firestore (all plans)
- Last updated: March 2026

## 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.

## Before you start

- 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.

```
import { doc, setDoc, serverTimestamp } from 'firebase/firestore'

// Use email as document ID — guarantees one doc per email
async function createUserProfile(email: string, displayName: string) {
  const userRef = doc(db, 'user_profiles', email)

  await setDoc(userRef, {
    email,
    displayName,
    createdAt: serverTimestamp(),
  })
  // If called twice with the same email, the second call
  // overwrites the first — no duplicate created
}

// Use a hash for composite uniqueness
// e.g., prevent a user from joining the same group twice
async function joinGroup(userId: string, groupId: string) {
  const membershipId = `${userId}_${groupId}`
  const ref = doc(db, 'memberships', membershipId)

  await setDoc(ref, {
    userId,
    groupId,
    joinedAt: serverTimestamp(),
  })
}
```

**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.

```
import { doc, runTransaction, serverTimestamp } from 'firebase/firestore'

async function createUniqueUsername(userId: string, username: string) {
  const usernameRef = doc(db, 'usernames', username.toLowerCase())

  await runTransaction(db, async (transaction) => {
    const usernameDoc = await transaction.get(usernameRef)

    if (usernameDoc.exists()) {
      throw new Error(`Username "${username}" is already taken`)
    }

    // Create the username reservation
    transaction.set(usernameRef, {
      userId,
      createdAt: serverTimestamp(),
    })

    // Also update the user's profile with the username
    transaction.update(doc(db, 'users', userId), {
      username: username.toLowerCase(),
    })
  })
}
```

**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.

```
// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Username reservations: only allow create, never overwrite
    match /usernames/{username} {
      allow create: if request.auth != null
        && resource == null                      // Document must not exist
        && request.resource.data.userId == request.auth.uid; // Must be their own

      allow read: if true;                       // Anyone can check availability

      allow delete: if request.auth != null
        && resource.data.userId == request.auth.uid; // Only owner can delete

      allow update: if false;                    // Never update, only create/delete
    }
  }
}
```

**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.

```
import { doc, runTransaction, collection, serverTimestamp } from 'firebase/firestore'

async function createUser(email: string, displayName: string) {
  // Lookup collection: emails/{email} → { userId }
  const emailRef = doc(db, 'emails', email.toLowerCase())

  await runTransaction(db, async (transaction) => {
    // Check if email is already registered
    const emailDoc = await transaction.get(emailRef)
    if (emailDoc.exists()) {
      throw new Error('Email is already registered')
    }

    // Create the user document with auto-generated ID
    const userRef = doc(collection(db, 'users'))

    // Create both atomically
    transaction.set(userRef, {
      email: email.toLowerCase(),
      displayName,
      createdAt: serverTimestamp(),
    })

    transaction.set(emailRef, {
      userId: userRef.id,
      createdAt: serverTimestamp(),
    })
  })
}

// To check availability without creating:
async function isEmailAvailable(email: string): Promise<boolean> {
  const emailDoc = await getDoc(doc(db, 'emails', email.toLowerCase()))
  return !emailDoc.exists()
}
```

**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.

```
import { doc, setDoc, serverTimestamp } from 'firebase/firestore'

// Idempotent write: safe to execute multiple times
async function submitVote(userId: string, pollId: string, choice: string) {
  // Deterministic ID: one vote per user per poll
  const voteId = `${userId}_${pollId}`
  const voteRef = doc(db, 'votes', voteId)

  // setDoc overwrites if offline duplicate syncs later
  await setDoc(voteRef, {
    userId,
    pollId,
    choice,
    votedAt: serverTimestamp(),
  })
  // Even if called twice offline, only one vote document exists
}
```

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

## Complete code example

File: `prevent-duplicates.ts`

```typescript
import { initializeApp } from 'firebase/app'
import {
  getFirestore,
  doc,
  getDoc,
  setDoc,
  runTransaction,
  collection,
  serverTimestamp,
} from 'firebase/firestore'

const app = initializeApp({
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,
})
const db = getFirestore(app)

// Pattern 1: Deterministic ID
export async function setProfile(email: string, name: string) {
  await setDoc(doc(db, 'profiles', email.toLowerCase()), {
    email: email.toLowerCase(),
    name,
    updatedAt: serverTimestamp(),
  })
}

// Pattern 2: Transaction-based conditional create
export async function reserveUsername(userId: string, username: string) {
  const key = username.toLowerCase()
  const ref = doc(db, 'usernames', key)

  await runTransaction(db, async (tx) => {
    const snap = await tx.get(ref)
    if (snap.exists()) throw new Error('Username taken')
    tx.set(ref, { userId, createdAt: serverTimestamp() })
    tx.update(doc(db, 'users', userId), { username: key })
  })
}

// Pattern 3: Lookup collection
export async function createUserWithUniqueEmail(
  email: string,
  displayName: string
) {
  const emailRef = doc(db, 'emails', email.toLowerCase())
  await runTransaction(db, async (tx) => {
    const snap = await tx.get(emailRef)
    if (snap.exists()) throw new Error('Email already registered')
    const userRef = doc(collection(db, 'users'))
    tx.set(userRef, { email: email.toLowerCase(), displayName, createdAt: serverTimestamp() })
    tx.set(emailRef, { userId: userRef.id, createdAt: serverTimestamp() })
  })
}

// Pattern 4: Check availability
export async function isAvailable(lookupCollection: string, value: string) {
  const snap = await getDoc(doc(db, lookupCollection, value.toLowerCase()))
  return !snap.exists()
}
```

## Common mistakes

- **Using addDoc for data that should be unique, resulting in multiple documents with the same value** — undefined Fix: Use setDoc with a deterministic document ID derived from the unique value. addDoc always creates a new document with a random ID.
- **Checking for existence with getDoc before creating with setDoc outside a transaction, allowing race conditions** — undefined Fix: 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.
- **Forgetting to normalize values (lowercase) before using them as keys, treating 'John' and 'john' as different** — undefined Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/firebase-tutorial/how-to-prevent-duplicate-documents-in-firestore
© RapidDev — https://www.rapidevelopers.com/firebase-tutorial/how-to-prevent-duplicate-documents-in-firestore
