<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ firestore - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ firestore - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 07 Sep 2026 23:54:08 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/firestore/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How Firestore Structures Data and How to Perform CRUD Operations With It ]]>
                </title>
                <description>
                    <![CDATA[ Most apps eventually need to store and manipulate data. And if you're building with Firebase, that data lives in Firestore, Google's flexible, scalable NoSQL document database. But before you can conf ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-firestore-structures-data-and-how-to-perform-crud-operations-with-it/</link>
                <guid isPermaLink="false">6a95f8e758184cca726442fe</guid>
                
                    <category>
                        <![CDATA[ firestore ]]>
                    </category>
                
                    <category>
                        <![CDATA[ NoSQL ]]>
                    </category>
                
                    <category>
                        <![CDATA[ crud ]]>
                    </category>
                
                    <category>
                        <![CDATA[ database ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Caleb Mintoumba ]]>
                </dc:creator>
                <pubDate>Mon, 31 Aug 2026 21:57:59 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/6b7f594e-36d9-48b5-a1eb-ad2d19f4d253.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Most apps eventually need to store and manipulate data. And if you're building with Firebase, that data lives in Firestore, Google's flexible, scalable NoSQL document database.</p>
<p>But before you can confidently create, read, update, or delete data, you need to understand how Firestore actually organizes information. It doesn't look like a SQL database, and treating it like one is the fastest way to end up with a messy, hard-to-query data structure.</p>
<p>In this tutorial, you'll learn how Firestore's NoSQL data model works, then build a small task management app to practice every CRUD operation with the Firebase Web SDK (v9+, modular). By the end, you'll be able to add tasks, query them, update nested fields and arrays, and delete data safely without leaving orphaned subcollections behind.</p>
<h3 id="heading-table-of-contents">Table of Contents</h3>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-how-firestore-structures-data">How Firestore Structures Data</a></p>
</li>
<li><p><a href="#heading-step-1-set-up-your-firebase-project">Step 1 – Set Up Your Firebase Project</a></p>
</li>
<li><p><a href="#heading-step-2-initialize-the-sdk">Step 2 – Initialize the SDK</a></p>
</li>
<li><p><a href="#heading-step-3-create-adding-tasks">Step 3 – Create: Adding Tasks</a></p>
</li>
<li><p><a href="#heading-step-4-read-querying-tasks">Step 4 – Read: Querying Tasks</a></p>
</li>
<li><p><a href="#heading-step-5-update-modifying-tasks">Step 5 – Update: Modifying Tasks</a></p>
</li>
<li><p><a href="#heading-step-6-delete-removing-tasks">Step 6 – Delete: Removing Tasks</a></p>
</li>
<li><p><a href="#heading-debugging-common-issues">Debugging Common Issues</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>Before you start, make sure you have the following:</p>
<ul>
<li><p><strong>Node.js v18 or later</strong> (<code>node --version</code>)</p>
</li>
<li><p>A <strong>Google account</strong> to create a Firebase project (the free Spark plan is enough for this tutorial)</p>
</li>
<li><p>Basic familiarity with JavaScript, including <code>async</code>/<code>await</code> and ES modules</p>
</li>
<li><p>A code editor and a terminal</p>
</li>
</ul>
<p>You don't need prior experience with Firebase or NoSQL databases, as this guide builds that understanding from the ground up.</p>
<h2 id="heading-how-firestore-structures-data">How Firestore Structures Data</h2>
<p>If you're coming from a relational (SQL) background, the first thing to unlearn is the idea of tables with a fixed schema and foreign key joins. Firestore is a <strong>document-oriented NoSQL database</strong>, and it organizes data around two core concepts: <strong>collections</strong> and <strong>documents</strong>.</p>
<ul>
<li><p>A collection is a named bucket that holds documents. Think <code>tasks</code>, <code>users</code>, or <code>orders</code>.</p>
</li>
<li><p>A document is a single record inside a collection, identified by a unique ID. It stores data as key-value pairs, similar to a JSON object.</p>
</li>
</ul>
<p>Here's the catch that trips up a lot of newcomers: <strong>documents don't need to share the same fields</strong>. One <code>task</code> document can have a <code>dueDate</code> field while another doesn't. Firestore doesn't enforce a schema at the database level, that responsibility shifts to your application code.</p>
<h4 id="heading-nesting-and-subcollections">Nesting and subcollections</h4>
<p>Documents can hold two kinds of nested data:</p>
<ul>
<li><p><strong>Maps</strong>, which are objects nested directly inside a document (for example, a <code>metadata</code> field containing <code>{ priority, dueDate }</code>)</p>
</li>
<li><p><strong>Subcollections</strong>, which are entire collections nested under a specific document (for example, every task can have its own <code>comments</code> subcollection)</p>
</li>
</ul>
<p>This gives you a structure that looks like a tree:</p>
<pre><code class="language-plaintext">tasks (collection)
 └── taskId (document)
      ├── title: "Article title"
      ├── completed: false
      ├── tags: ["writing", "firebase"]
      ├── metadata: { priority: "high", dueDate: &lt;timestamp&gt; }
      └── comments (subcollection)
           └── commentId (document)
                ├── text: "CRUD Article"
                └── createdAt: &lt;timestamp&gt;
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/66f71ee288cc311f84e563bc/8c16db01-6335-4d00-92c0-8bbf392bd2e9.jpg" alt="A tree diagram illustrating Firestore's data hierarchy: a &quot;tasks&quot; collection contains a &quot;taskId&quot; document, which holds fields such as title, completed, tags, and a nested metadata map, alongside a &quot;comments&quot; subcollection containing individual comment documents with their own text and createdAt fields" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h4 id="heading-supported-data-types">Supported data types</h4>
<p>Firestore documents can store several native types. The ones you'll use most often are:</p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Example</th>
</tr>
</thead>
<tbody><tr>
<td><code>string</code></td>
<td><code>"Write CRUD article"</code></td>
</tr>
<tr>
<td><code>number</code></td>
<td><code>42</code></td>
</tr>
<tr>
<td><code>boolean</code></td>
<td><code>true</code></td>
</tr>
<tr>
<td><code>array</code></td>
<td><code>["writing", "firebase"]</code></td>
</tr>
<tr>
<td><code>map</code></td>
<td><code>{ priority: "high" }</code></td>
</tr>
<tr>
<td><code>timestamp</code></td>
<td><code>Timestamp.now()</code></td>
</tr>
<tr>
<td><code>reference</code></td>
<td>a pointer to another document</td>
</tr>
<tr>
<td><code>geopoint</code></td>
<td>a latitude/longitude pair</td>
</tr>
</tbody></table>
<h4 id="heading-why-this-matters-before-writing-crud-code">Why this matters before writing CRUD code</h4>
<p>Every CRUD operation you'll write later depends on this structure:</p>
<ul>
<li><p><strong>Create</strong> means adding a document to a collection, with an auto-generated or custom ID.</p>
</li>
<li><p><strong>Read</strong> means fetching either a single document by ID or a set of documents matching a query.</p>
</li>
<li><p><strong>Update</strong> means modifying fields on an existing document, including nested maps and arrays.</p>
</li>
<li><p><strong>Delete</strong> means removing a document, and Firestore will <em>not</em> automatically clean up its subcollections (a common gotcha you'll see in Step 6).</p>
</li>
</ul>
<p>With the mental model in place, let's set up a project and start writing code.</p>
<h3 id="heading-step-1-set-up-your-firebase-project">Step 1 – Set Up Your Firebase Project</h3>
<p>Head to the <a href="https://console.firebase.google.com/">Firebase console</a> and create a new project.</p>
<ol>
<li><p>Click <strong>Add project</strong>, give it a name (for example: <code>crud-tasks-demo</code>), and follow the setup wizard (Google Analytics is optional for this tutorial).</p>
</li>
<li><p>Once the project is created, open the left sidebar and click <strong>Databases and Storage</strong> and then <strong>Firestore</strong>.</p>
</li>
<li><p>Click <strong>Create database</strong>. Choose a location close to you, and for this tutorial, start in <strong>test mode</strong> so you can read and write without configuring security rules yet.</p>
</li>
</ol>
<p><strong>Note:</strong> Test mode leaves your database open to anyone for 30 days. Never ship an app to production without proper <a href="https://firebase.google.com/docs/firestore/security/get-started">Firestore security rules</a>, we'll touch on this in the Debugging section.</p>
<p>You should now see an empty Firestore database, ready to receive your first collection.</p>
<h3 id="heading-step-2-initialize-the-sdk">Step 2 – Initialize the SDK</h3>
<p>Create a new project folder and install the Firebase Web SDK:</p>
<pre><code class="language-shell">mkdir firestore-crud-demo &amp;&amp; cd firestore-crud-demo
npm init -y
npm install firebase
</code></pre>
<p>Grab your project's config object from <strong>Project settings - General - Your apps - Web app</strong> in the Firebase console (register a new web app if you haven't yet).</p>
<p>Create a <code>firebase-config.js</code> file:</p>
<pre><code class="language-javascript">// firebase-config.js
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
  projectId: "YOUR_PROJECT_ID",
  storageBucket: "YOUR_PROJECT_ID.appspot.com",
  messagingSenderId: "YOUR_SENDER_ID",
  appId: "YOUR_APP_ID",
};

const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);
</code></pre>
<p>Every CRUD example from here on imports <code>db</code> from this file. Keep your actual config values out of version control (use environment variables in a real project).</p>
<h3 id="heading-step-3-create-adding-tasks">Step 3 – Create: Adding Tasks</h3>
<p>Firestore gives you two ways to create a document: let Firestore generate the ID, or set your own.</p>
<h4 id="heading-auto-generated-id-with-adddoc">Auto-generated ID with <code>addDoc()</code></h4>
<pre><code class="language-javascript">// create-task.js
import { collection, addDoc, Timestamp } from "firebase/firestore";
import { db } from "./firebase-config.js";

async function createTask() {
  try {
    const docRef = await addDoc(collection(db, "tasks"), {
      title: "Write CRUD article",
      completed: false,
      tags: ["writing", "firebase"],
      metadata: {
        priority: "high",
        dueDate: Timestamp.fromDate(new Date("2026-09-15")),
      },
      createdAt: Timestamp.now(),
    });
    console.log("Task created with ID:", docRef.id);
  } catch (error) {
    console.error("Error creating task:", error);
  }
}

createTask();
</code></pre>
<h4 id="heading-custom-id-with-setdoc"><strong>Custom ID with</strong> <code>setDoc()</code></h4>
<p>Use this when you want to control the document ID yourself, for example, matching it to an ID from another system.</p>
<pre><code class="language-javascript">import { doc, setDoc } from "firebase/firestore";
import { db } from "./firebase-config.js";

async function createTaskWithId(taskId) {
  await setDoc(doc(db, "tasks", taskId), {
    title: "Review pull request",
    completed: false,
    tags: ["code-review"],
  });
}

createTaskWithId("task-001");
</code></pre>
<h4 id="heading-adding-a-document-to-a-subcollection">Adding a document to a subcollection</h4>
<p>To add a comment under a specific task, you reference the parent document first:</p>
<pre><code class="language-javascript">import { collection, addDoc, Timestamp } from "firebase/firestore";
import { db } from "./firebase-config.js";

async function addComment(taskId, text) {
  await addDoc(collection(db, "tasks", taskId, "comments"), {
    text,
    createdAt: Timestamp.now(),
  });
}

addComment("task-001", "First draft done");
</code></pre>
<h3 id="heading-step-4-read-querying-tasks">Step 4 – Read: Querying Tasks</h3>
<h4 id="heading-fetching-a-single-document">Fetching a single document</h4>
<pre><code class="language-javascript">import { doc, getDoc } from "firebase/firestore";
import { db } from "./firebase-config.js";

async function getTask(taskId) {
  const snapshot = await getDoc(doc(db, "tasks", taskId));
  if (snapshot.exists()) {
    console.log(snapshot.id, snapshot.data());
  } else {
    console.log("No such task.");
  }
}

getTask("task-001");
</code></pre>
<h4 id="heading-fetching-an-entire-collection">Fetching an entire collection</h4>
<pre><code class="language-javascript">import { collection, getDocs } from "firebase/firestore";
import { db } from "./firebase-config.js";

async function getAllTasks() {
  const snapshot = await getDocs(collection(db, "tasks"));
  snapshot.forEach((doc) =&gt; {
    console.log(doc.id, doc.data());
  });
}

getAllTasks();
</code></pre>
<h4 id="heading-filtering-with-queries">Filtering with queries</h4>
<pre><code class="language-javascript">import { collection, query, where, orderBy, limit, getDocs } from "firebase/firestore";
import { db } from "./firebase-config.js";

async function getUrgentPendingTasks() {
  const q = query(
    collection(db, "tasks"),
    where("completed", "==", false),
    orderBy("metadata.priority"),
    limit(10)
  );

  const snapshot = await getDocs(q);
  snapshot.forEach((doc) =&gt; console.log(doc.id, doc.data()));
}

getUrgentPendingTasks();
</code></pre>
<p><strong>Heads up:</strong> combining <code>where()</code> on one field with <code>orderBy()</code> on another often requires a <strong>composite index</strong>. Firestore will throw an error in your console with a direct link to create it. More on this in Debugging.</p>
<h4 id="heading-real-time-updates-with-onsnapshot">Real-time updates with <code>onSnapshot()</code></h4>
<p>Instead of fetching once, you can subscribe to live changes. This is useful for a task list that updates instantly across devices:</p>
<pre><code class="language-javascript">import { collection, onSnapshot } from "firebase/firestore";
import { db } from "./firebase-config.js";

const unsubscribe = onSnapshot(collection(db, "tasks"), (snapshot) =&gt; {
  snapshot.docChanges().forEach((change) =&gt; {
    console.log(change.type, change.doc.id, change.doc.data());
  });
});

// Call unsubscribe() when you no longer need updates (e.g., component unmount)
</code></pre>
<h3 id="heading-step-5-update-modifying-tasks">Step 5 – Update: Modifying Tasks</h3>
<h4 id="heading-partial-update-with-updatedoc">Partial update with <code>updateDoc()</code></h4>
<p>Unlike <code>setDoc()</code>, <code>updateDoc()</code> only touches the fields you specify. Everything else on the document stays untouched.</p>
<pre><code class="language-javascript">import { doc, updateDoc } from "firebase/firestore";
import { db } from "./firebase-config.js";

async function completeTask(taskId) {
  await updateDoc(doc(db, "tasks", taskId), {
    completed: true,
  });
}

completeTask("task-001");
</code></pre>
<h4 id="heading-updating-a-nested-field-with-dot-notation">Updating a nested field with dot notation</h4>
<p>You don't need to rewrite the whole <code>metadata</code> map to change one property inside it:</p>
<pre><code class="language-javascript">await updateDoc(doc(db, "tasks", "task-001"), {
  "metadata.priority": "low",
});
</code></pre>
<h4 id="heading-updating-arrays-safely">Updating arrays safely</h4>
<p>Directly overwriting an array field is risky in concurrent scenarios. Use <code>arrayUnion()</code> and <code>arrayRemove()</code> instead:</p>
<pre><code class="language-javascript">import { doc, updateDoc, arrayUnion, arrayRemove } from "firebase/firestore";
import { db } from "./firebase-config.js";

async function addTag(taskId, tag) {
  await updateDoc(doc(db, "tasks", taskId), {
    tags: arrayUnion(tag),
  });
}

async function removeTag(taskId, tag) {
  await updateDoc(doc(db, "tasks", taskId), {
    tags: arrayRemove(tag),
  });
}
</code></pre>
<p><code>arrayUnion()</code> won't add a duplicate value, and <code>arrayRemove()</code> removes every matching instance. Both operate atomically on the server.</p>
<h3 id="heading-step-6-delete-removing-tasks">Step 6 – Delete: Removing Tasks</h3>
<h4 id="heading-deleting-a-document">Deleting a document</h4>
<pre><code class="language-javascript">import { doc, deleteDoc } from "firebase/firestore";
import { db } from "./firebase-config.js";

async function deleteTask(taskId) {
  await deleteDoc(doc(db, "tasks", taskId));
}

deleteTask("task-001");
</code></pre>
<h4 id="heading-the-subcollection-trap">The subcollection trap</h4>
<p>Here's the gotcha mentioned earlier: deleting <code>tasks/task-001</code> does <strong>not</strong> delete its <code>comments</code> subcollection. Those comment documents become orphaned, they still exist in your database. They're just unreachable through the UI unless you know the path.</p>
<p>To clean up properly, delete the subcollection's documents first, then the parent:</p>
<pre><code class="language-javascript">import { collection, getDocs, doc, deleteDoc, writeBatch } from "firebase/firestore";
import { db } from "./firebase-config.js";

async function deleteTaskWithComments(taskId) {
  const commentsRef = collection(db, "tasks", taskId, "comments");
  const commentsSnapshot = await getDocs(commentsRef);

  const batch = writeBatch(db);
  commentsSnapshot.forEach((commentDoc) =&gt; {
    batch.delete(commentDoc.ref);
  });
  batch.delete(doc(db, "tasks", taskId));

  await batch.commit();
}

deleteTaskWithComments("task-001");
</code></pre>
<p><code>writeBatch()</code> groups multiple deletes into one atomic operation. Either all of them succeed, or none do.</p>
<h4 id="heading-deleting-a-single-field">Deleting a single field</h4>
<p>If you only want to remove one field without deleting the whole document, use <code>deleteField()</code>:</p>
<pre><code class="language-javascript">import { doc, updateDoc, deleteField } from "firebase/firestore";
import { db } from "./firebase-config.js";

await updateDoc(doc(db, "tasks", "task-001"), {
  metadata: deleteField(),
});
</code></pre>
<h3 id="heading-debugging-common-issues">Debugging Common Issues</h3>
<h4 id="heading-firebaseerror-missing-or-insufficient-permissions"><code>FirebaseError: Missing or insufficient permissions</code></h4>
<p>Your security rules are blocking the request. If you're still in test mode, check whether your 30-day window expired (rules revert to deny-all after that). For a real app, review your rules in <strong>Firestore</strong> and then <strong>Rules</strong> and make sure they match the paths you're reading/writing, including subcollections, which need their own rule blocks.</p>
<h4 id="heading-function-adddoc-called-with-invalid-data-unsupported-field-value-undefined"><code>Function addDoc() called with invalid data. Unsupported field value: undefined</code></h4>
<p>Firestore rejects <code>undefined</code> values outright, unlike <code>null</code>, which is allowed. This usually happens when a form field is empty and you pass it straight into your write call. Filter out <code>undefined</code> fields before writing, or default them to <code>null</code>.</p>
<h4 id="heading-the-query-requires-an-index"><code>The query requires an index</code></h4>
<p>This shows up when you combine <code>where()</code> and <code>orderBy()</code> on different fields, as in the Step 4 example. Firestore can't serve that query with automatic indexes. The error message includes a direct link that pre-fills the composite index for you in the console, click it, wait a minute or two for the index to build, and rerun your query.</p>
<h4 id="heading-reads-adding-up-fast-quota-warnings">Reads adding up fast / quota warnings</h4>
<p>Every document returned by <code>getDocs()</code> counts as a read, even inside a loop calling <code>getDoc()</code> repeatedly. Avoid fetching a whole collection just to filter it client-side, push filtering into your query with <code>where()</code> instead, and use <code>limit()</code> on anything that could grow unbounded.</p>
<h4 id="heading-orphaned-subcollections-after-delete">Orphaned subcollections after delete</h4>
<p>If you notice documents you thought you deleted still consuming storage or showing up in exports, check for subcollections under the deleted document's path. As shown in Step 6, <code>deleteDoc()</code> never cascades, cleanup is always your responsibility.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66f71ee288cc311f84e563bc/6f5d1798-3798-4e05-9ceb-073d8857e15c.jpg" alt="A circular flow diagram showing the four CRUD operations as a continuous cycle, Create, Read, Update, and Delete, each labeled with its corresponding Firestore JavaScript functions (addDoc/setDoc, getDoc/getDocs/onSnapshot, updateDoc/arrayUnion, deleteDoc/writeBatch), illustrating how these operations connect in a typical data lifecycle." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h2 id="heading-conclusion">Conclusion</h2>
<p>You now have a working mental model of Firestore's structure and hands-on experience with every CRUD operation using the Web SDK v9+. Here's a quick recap:</p>
<table>
<thead>
<tr>
<th>Operation</th>
<th>Key functions</th>
</tr>
</thead>
<tbody><tr>
<td>Create</td>
<td><code>addDoc()</code>, <code>setDoc()</code></td>
</tr>
<tr>
<td>Read</td>
<td><code>getDoc()</code>, <code>getDocs()</code>, <code>query()</code>, <code>onSnapshot()</code></td>
</tr>
<tr>
<td>Update</td>
<td><code>updateDoc()</code>, <code>arrayUnion()</code>, <code>arrayRemove()</code></td>
</tr>
<tr>
<td>Delete</td>
<td><code>deleteDoc()</code>, <code>deleteField()</code>, <code>writeBatch()</code></td>
</tr>
</tbody></table>
<p>From here, there are a few natural next steps once you're comfortable with the basics:</p>
<ul>
<li><p><strong>Transactions</strong>, for reads and writes that must succeed or fail together (for example, transferring a task between two users)</p>
</li>
<li><p><strong>Batch writes</strong>, which you already saw in Step 6. They're useful anytime you need to touch multiple documents atomically</p>
</li>
<li><p><strong>Composite indexes</strong>, for more advanced filtering and sorting combinations</p>
</li>
<li><p><strong>Pagination</strong> with <code>startAfter()</code>, for loading large collections in chunks instead of all at once</p>
</li>
</ul>
<p>If you haven't already, it's worth revisiting how to model your data <em>before</em> you write queries against it. Decisions made at the modeling stage (like whether to nest data or use a subcollection) directly shape which of these CRUD patterns will feel natural versus awkward later on.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Firestore Data Modeling Guide: Embedded Documents vs Referencing (with a Blog Case Study) ]]>
                </title>
                <description>
                    <![CDATA[ When developers transition from the relational world (MySQL, PostgreSQL) to Firestore, Firebase's NoSQL document database, they often bring their old habits with them. They try to replicate tables, fo ]]>
                </description>
                <link>https://www.freecodecamp.org/news/firestore-data-modeling-guide-embedded-documents-vs-referencing-with-a-blog-case-study/</link>
                <guid isPermaLink="false">6a63826ed2f5d140f2aaa325</guid>
                
                    <category>
                        <![CDATA[ firestore ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Firebase ]]>
                    </category>
                
                    <category>
                        <![CDATA[ NoSQL ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Databases ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Query ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SQL ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Caleb Mintoumba ]]>
                </dc:creator>
                <pubDate>Fri, 24 Jul 2026 15:19:10 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/f0166ca3-ca48-45f6-bb2f-ee6b20701ea0.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When developers transition from the relational world (MySQL, PostgreSQL) to Firestore, Firebase's NoSQL document database, they often bring their old habits with them. They try to replicate tables, foreign keys, and joins.</p>
<p>The result? Complex queries, skyrocketing read costs, and a database structure that becomes a nightmare to maintain after just a few features.</p>
<p>To understand how Firestore works, we first need to look at our point of comparison: the relational model. Once we map out how SQL does things, we can see exactly where Firestore diverges, and how to structure NoSQL data correctly.</p>
<p>In this guide, we'll cover NoSQL design principles, embedding vs. referencing, and relationship modeling (1-1, 1-N, N-N). We'll also walk through a concrete blog case study.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-the-relational-mindset-how-sql-handles-data">The Relational Mindset: How SQL Handles Data</a></p>
</li>
<li><p><a href="#heading-the-firestore-paradigm-nosql-with-relationships">The Firestore Paradigm: NoSQL with Relationships</a></p>
</li>
<li><p><a href="#heading-the-core-building-blocks-documents-and-collections">The Core Building Blocks: Documents and Collections</a></p>
</li>
<li><p><a href="#heading-the-golden-rule-model-for-reads-not-writes">The Golden Rule: Model for Reads, Not Writes</a></p>
</li>
<li><p><a href="#heading-embedding-vs-referencing-denormalization">Embedding vs. Referencing (Denormalization)</a></p>
</li>
<li><p><a href="#heading-how-to-model-relationships-1-1-1-n-n-n">How to Model Relationships (1-1, 1-N, N-N)</a></p>
</li>
<li><p><a href="#heading-best-practices-and-pitfalls-to-avoid">Best Practices and Pitfalls to Avoid</a></p>
</li>
<li><p><a href="#heading-case-study-designing-a-scalable-blog-database">Case Study: Designing a Scalable Blog Database</a></p>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>This guide is conceptual, so you don't need a running Firestore project to follow along. A little context is enough. You will need:</p>
<ul>
<li><p>Basic JavaScript syntax, since every code example uses the modular Firebase JS SDK (v9+)</p>
</li>
<li><p>Basic familiarity with JSON objects (keys, values, nested objects, arrays)</p>
</li>
<li><p>Some exposure to SQL or relational databases helps, since the guide leans on that comparison throughout (but it's not required)</p>
</li>
<li><p>(Optional) A free Firebase project, if you want to try the examples yourself. The <a href="https://firebase.google.com/docs/firestore/quickstart">Firestore quickstart</a> walks you through setting one up.</p>
</li>
</ul>
<p>No prior NoSQL or Firestore experience is needed.</p>
<h2 id="heading-the-relational-mindset-how-sql-handles-data">The Relational Mindset: How SQL Handles Data</h2>
<p>In a relational database, data is organized into tables linked by explicit relationships. This approach relies on <strong>normalization</strong> to eliminate data redundancy.</p>
<p>For example, to store users and their respective countries, we split the data into two tables:</p>
<ul>
<li><p><code>Users</code>: columns <code>id</code> (PK), <code>last_name</code>, <code>first_name</code>, <code>#country_id</code> (FK a foreign key)</p>
</li>
<li><p><code>Countries</code>: columns <code>country_id</code> (PK), <code>country_name</code></p>
</li>
</ul>
<p>With a row like <code>1, MINTOUMBA, Caleb, 1</code> in <code>Users</code> and <code>1, Canada</code> in <code>Countries</code>, we automatically know that Caleb belongs to Canada through the foreign key <code>#country_id</code>. We never had to write the word "Canada" inside the <code>Users</code> table itself.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66f71ee288cc311f84e563bc/ac10a248-0b5e-4be7-a738-3a6bdd54c1d7.png" alt="Relational model showing a Users table linked to a Countries table through a foreign key" style="display:block;margin:0 auto" width="2179" height="1019" loading="lazy">

<p><strong>The SQL trade-off:</strong> writes are lightweight (you only update data in one place), but reads are heavier, because you have to perform a database join (<code>JOIN</code>) every time you want to display a user's country name.</p>
<p>That's exactly the opposite of how Firestore works, as we'll see next.</p>
<h2 id="heading-the-firestore-paradigm-nosql-with-relationships">The Firestore Paradigm: NoSQL with Relationships</h2>
<p>Firestore is a <strong>NoSQL</strong> document database – literally <em>Not Only SQL</em>. It stores JSON-like documents grouped into collections, with no enforced schema.</p>
<p>For most of Firestore's history, that also meant no native joins and no <code>GROUP BY</code>. The standard query engine simply didn't support them, and any aggregation beyond <code>count()</code>, <code>sum()</code>, and <code>average()</code> had to happen in your application code.</p>
<p>That's still true today for <strong>Standard edition</strong>, which remains the default and the one most mobile/web apps run on and the one this guide focuses on.</p>
<p>Google has since introduced <strong>Firestore Enterprise edition</strong>, built around a new <strong>Pipeline</strong> query engine that reached general availability in April 2026. Pipelines add a multi-stage query syntax and hundreds of new functions, including relational-style joins through correlated subqueries and a real <code>aggregate(...)</code> step with grouping Firestore's equivalent of SQL's <code>GROUP BY</code>.</p>
<p><strong>Does this mean data modeling doesn't matter anymore?</strong> Not for most apps. Pipeline queries run within a 60-second timeout and a 128 MiB working-memory limit, can fall back to full collection scans when no index exists, and critically, Enterprise edition drops real-time listeners and offline support (which most Firestore client apps depend on).</p>
<p>Pipelines are a genuine escape hatch for analytical, admin, or reporting queries. They're not a drop-in replacement for the read-optimized structure your app's everyday screens still need.</p>
<p>If you're building a typical client-facing app on Standard edition, the embedding and denormalization strategies below are still how you model relationships.</p>
<p>But <strong>NoSQL doesn't mean "no relationships"</strong> even on Standard edition. You can and should build robust relationships between your collections. The difference is that Firestore won't enforce or resolve them for you the way a <code>JOIN</code> does by default. It's up to you, the developer, to build and query those relationships explicitly, and to maintain data integrity through your application code or Cloud Functions unless you've specifically opted into Enterprise edition for Pipeline-powered joins.</p>
<h2 id="heading-the-core-building-blocks-documents-and-collections">The Core Building Blocks: Documents and Collections</h2>
<p>Before designing any schema, let's define Firestore's two core building blocks:</p>
<ul>
<li><p><strong>Document</strong>: the basic unit of storage. It's a JSON-like object, identified by a unique ID, containing typed fields (strings, numbers, booleans, timestamps, geopoints, or references to other documents).</p>
</li>
<li><p><strong>Collection</strong>: a container for documents. Unlike SQL tables, documents in the same collection don't need to share the same structure.</p>
</li>
</ul>
<p>What makes Firestore unique is its hierarchical nature: <strong>a document can contain sub-collections</strong>, which contain more documents, which can themselves contain more sub-collections, and so on.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66f71ee288cc311f84e563bc/3d49ff84-8648-4c15-bcc3-60caeb540a77.png" alt="Firestore hierarchy diagram showing a posts collection containing the post_001 document, which holds a comments sub-collection with individual comment documents" style="display:block;margin:0 auto" width="2179" height="1259" loading="lazy">

<p>In the diagram above, the root <code>posts</code> collection contains the document <code>post_001</code>, which itself hosts a <code>comments</code> sub-collection containing the individual comment documents <code>comment_001</code> and <code>comment_002</code>. You can nest collections and documents several levels deep, but as we'll see later, it's best to do so sparingly.</p>
<p><strong>Crucial rule:</strong> sub-collections are never retrieved automatically when you read a parent document. Unlike a SQL <code>JOIN</code>, you must always perform a separate, explicit query to read a sub-collection.</p>
<h2 id="heading-the-golden-rule-model-for-reads-not-writes">The Golden Rule: Model for Reads, Not Writes</h2>
<p>This is the single most important concept in NoSQL modeling, and the one developers coming from SQL forget most often: <strong>structure your data based on how your app queries it, not on how it gets written.</strong></p>
<p>Before writing any database code, ask yourself:</p>
<ul>
<li><p>Which screens in my app will display this data?</p>
</li>
<li><p>Do I need this piece of data on its own, or always alongside another one?</p>
</li>
<li><p>Do I read this information significantly more often than I write or update it?</p>
</li>
</ul>
<p>If your users view a writer's profile 10,000 times for every single time that writer updates their username, optimize for the reads: duplicate the username directly inside each post. That's the exact opposite of the SQL instinct we saw earlier, where you normalize first to avoid redundancy, even if it makes reads heavier.</p>
<h2 id="heading-embedding-vs-referencing-denormalization">Embedding vs. Referencing (Denormalization)</h2>
<p>There are two primary strategies for representing a relationship in Firestore.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66f71ee288cc311f84e563bc/ac6d31d1-9c75-4688-b39a-e01ffa55ea07.png" alt="Side-by-side comparison of embedding comments directly inside a post document versus referencing them through a separate comments sub-collection" style="display:block;margin:0 auto" width="2179" height="1180" loading="lazy">

<h3 id="heading-option-a-embedding-nesting">Option A: Embedding (Nesting)</h3>
<p>You store the related data directly inside the parent document, as an array or a map (object).</p>
<pre><code class="language-js">// A post with its comments embedded
{
  title: "Introduction to Firestore",
  author: "Caleb",
  comments: [
    { user: "Ama", text: "Great post!" },
    { user: "Kofi", text: "Thanks for the examples" }
  ]
}
</code></pre>
<ul>
<li><p><strong>Pros</strong>: a single read retrieves everything, and consistency is guaranteed.</p>
</li>
<li><p><strong>Cons</strong>: Firestore documents have a hard <strong>1 MB size limit</strong>. If the nested list grows indefinitely (comments on a viral post, for instance), your writes will start failing once you hit that limit and every write to the parent document also re-sends the whole document to any client listening in real time.</p>
</li>
<li><p><strong>Best for</strong>: small, bounded lists (tags on an article, a user's settings, a short list of favorites).</p>
</li>
</ul>
<h3 id="heading-option-b-referencing-denormalization">Option B: Referencing (Denormalization)</h3>
<p>You split the entities into separate collections or sub-collections, and deliberately duplicate a few fields to avoid a second read.</p>
<pre><code class="language-js">// posts/post_001
{
  title: "Introduction to Firestore",
  authorId: "uid_123",
  authorName: "Caleb",      // denormalized: avoids a second read to "users"
  authorAvatar: "https://...",
  commentCount: 12          // denormalized counter
}

// posts/post_001/comments/comment_001
{
  userId: "uid_456",
  userName: "Ama",
  text: "Great post!",
  createdAt: Timestamp
}
</code></pre>
<p>Here, we duplicate the author's name and avatar into every post so we don't need an extra read to <code>users</code> every time the post list is displayed.</p>
<p>That's denormalization: we accept controlled redundancy in exchange for faster reads the exact opposite of SQL normalization. The cost is that these copies need updating if the user changes their name (usually handled by a Cloud Function triggered when the <code>users</code> document is updated).</p>
<ul>
<li><p><strong>Pros</strong>: no document size limits, and entities can be queried independently.</p>
</li>
<li><p><strong>Cons</strong>: requires multiple reads if you didn't denormalize enough. If a duplicated value changes, you need code (often a Cloud Function) to propagate the update everywhere it's copied.</p>
</li>
<li><p><strong>Best for</strong>: dynamic, fast-growing data (comments, order history, activity logs).</p>
</li>
</ul>
<p><strong>A more precise rule of thumb</strong>: whether to <em>reference instead of embed</em> depends on volume. Sub-collections handle unbounded growth (comments, order history) better than arrays.</p>
<p>Whether to <em>denormalize a given field</em> depends on the cost of keeping it in sync, not how often it changes: a counter you update in place with an atomic increment (<code>commentCount</code>, <code>likeCount</code>) has no other copy to synchronize, so it's cheap to denormalize regardless of frequency.</p>
<p>A copied value like <code>authorName</code>, on the other hand, is duplicated across every document that references it. It's safe to denormalize only if it changes rarely, since any change means propagating the update everywhere it's been copied.</p>
<h2 id="heading-how-to-model-relationships-1-1-1-n-n-n">How to Model Relationships (1-1, 1-N, N-N)</h2>
<h3 id="heading-one-to-one-1-1">One-to-One (1-1)</h3>
<p>Either embed the fields in the same document, or store them in a separate collection using the exact same document ID, for example <code>users/uid_123</code> and <code>privateProfiles/uid_123</code>. This is perfect for separating public data from sensitive data that needs different security rules.</p>
<h3 id="heading-one-to-many-1-n">One-to-Many (1-N)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/66f71ee288cc311f84e563bc/cc6057e0-f3c9-42dd-827b-4546033b6248.png" alt="One-to-many relationship diagram showing a post document linked to multiple comment documents through a sub-collection" style="display:block;margin:0 auto" width="2179" height="980" loading="lazy">

<p>There are three main options, depending on volume and query direction:</p>
<ol>
<li><p>A <strong>sub-collection</strong> (<code>posts/post_001/comments/*</code>) is ideal when you almost always query comments <em>through</em> their parent post, and volume can be large.</p>
</li>
<li><p>A <strong>root collection with a reference</strong> (<code>comments</code> with a <code>postId</code> field) is useful if you also need to query all comments by a given user, independently of the post (<code>where("userId", "==", uid)</code>).</p>
</li>
<li><p>Use an <strong>embedded array</strong> only if the volume stays small and bounded (see Option A above).</p>
</li>
</ol>
<h3 id="heading-many-to-many-n-n">Many-to-Many (N-N)</h3>
<p>This is the trickiest one in NoSQL, since there's no automatic join table like in SQL. There are three common patterns:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66f71ee288cc311f84e563bc/b8b96fab-5180-43fb-98fd-fe16faac162f.png" alt="Many-to-many relationship diagram showing a memberships junction collection linking users and groups" style="display:block;margin:0 auto" width="2179" height="1060" loading="lazy">

<p><strong>(1). Junction collection</strong> the equivalent of a SQL pivot table:</p>
<pre><code class="language-js">// memberships/{membershipId}
{
  userId: "uid_123",
  groupId: "group_789",
  role: "admin",
  joinedAt: Timestamp
}
</code></pre>
<p>You can then query <code>.where("userId", "==", uid)</code> to find all groups a user belongs to, or <code>.where("groupId", "==", gid)</code> to find all members of a group.</p>
<p><strong>(2). ID arrays on both sides</strong> (cross-denormalization):</p>
<pre><code class="language-js">// users/uid_123      -&gt; groupIds: ["group_789", "group_456"]
// groups/group_789   -&gt; memberIds: ["uid_123", "uid_456"]
</code></pre>
<p>Fast to read from either side, but reserve this for lists that stay small the 1 MB document limit and the cost of atomically updating long arrays both work against you at scale.</p>
<p><strong>(3). Hybrid approach</strong>, which is the most common pattern in practice: an array for a lightweight relationship rarely queried from the other side (a user's favorite posts), and a junction collection for a relationship queried frequently in both directions and prone to frequent changes (team memberships).</p>
<h2 id="heading-best-practices-and-pitfalls-to-avoid">Best Practices and Pitfalls to Avoid</h2>
<ul>
<li><p><strong>Limit nesting depth:</strong> Firestore allows sub-collections to be nested indefinitely, but beyond two or three levels, your queries and security rules become genuinely hard to maintain. Prefer flattening the structure with references when you can.</p>
</li>
<li><p><strong>Avoid auto-incremented document IDs:</strong> Sequential IDs (<code>user_1</code>, <code>user_2</code>, <code>user_3</code>...) can cause <em>hotspotting</em>: writes pile up on a narrow range of the index, which degrades performance at scale. Let Firestore generate random, evenly distributed IDs unless you have a specific reason not to.</p>
</li>
<li><p><strong>Watch out for composite indexes:</strong> Any query combining multiple <code>.where()</code> filters, or a <code>.where()</code> with an <code>.orderBy()</code> on a different field, requires a composite index. Plan for these during design rather than discovering them in production (Firestore's error messages include a direct link to auto-generate the missing index).</p>
</li>
<li><p><strong>Mind the write rate on "hot" documents:</strong> The recommended maximum <em>sustained</em> write rate to a single document is about <strong>1 write per second</strong>. A document updated very frequently by many different users a global like counter, for example becomes a bottleneck well before that. Firestore can absorb short bursts (5, 10, even 50 writes in one second) by queuing them, but sustained traffic above ~1 write/sec will start producing contention errors. The standard fix is a <em>sharded counter</em>: split the count across several sub-documents and sum them at read time.</p>
</li>
<li><p><strong>Use sub-collections deliberately:</strong> They're convenient, but they always require a separate query. If you almost always need the data together, embedding or denormalization will perform better.</p>
</li>
<li><p><strong>Design security rules alongside your data model:</strong> Firestore's security rules (<code>firestore.rules</code>) should be designed at the same time as your schema a poorly thought-out structure usually makes precise rules much harder to write.</p>
</li>
</ul>
<h2 id="heading-case-study-designing-a-scalable-blog-database">Case Study: Designing a Scalable Blog Database</h2>
<p>Let's bring every principle from this guide together with a concrete example: a blog with posts, comments, and likes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/66f71ee288cc311f84e563bc/d774e41d-28d6-4d0a-81cb-a7bd088992d9.png" alt="Complete Firestore schema for a blog application showing the posts, comments sub-collection, and likes collection" style="display:block;margin:0 auto" width="2379" height="1300" loading="lazy">

<pre><code class="language-js">// posts/{postId}
{
  title: "Modeling Firestore",
  slug: "modeling-firestore",
  authorId: "uid_123",
  authorName: "Caleb",         // denormalized: avoids a second read to "users"
  content: "...",
  tags: ["firebase", "nosql"], // embedded: small, bounded list
  commentCount: 3,             // denormalized counter
  likeCount: 47,               // denormalized counter (shard it if traffic is high)
  createdAt: Timestamp
}

// posts/{postId}/comments/{commentId}  → sub-collection: read together with the post
{
  userId: "uid_456",
  userName: "Ama",
  text: "Excellent article",
  createdAt: Timestamp
}

// likes/{likeId}  → root collection + reference
{                    // lets you quickly check if ONE user liked ONE post
  postId: "post_001",
  userId: "uid_456"
}
</code></pre>
<p>Each choice here answers a specific read pattern. Tags are always displayed alongside the post, so they're embedded. Comments can grow large in number and are almost always fetched together with their parent post, so they live in a sub-collection. Likes need to be queried both by post <em>and</em> by user to check whether <em>this</em> user already liked <em>this</em> post so they sit in a root collection with two indexable fields.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In SQL, you normalize to eliminate redundancy, and you pay for that choice at read time, via joins. In Firestore, it's the opposite: you accept controlled redundancy (denormalization) to make reads instant and cheap, at the cost of slightly heavier writes.</p>
<p>Modeling data in Firestore isn't about applying relational habits with a different syntax. It's a genuinely different way of thinking, centered on your app's read patterns.</p>
<p>Always ask "how will I read this data, and how often?" before choosing between embedding, referencing, or a sub-collection. Also, keep Firestore's concrete limits in mind (1 MB per document, composite indexes, hotspotting) from the design phase rather than discovering them in production.</p>
<p>That balance between read simplicity and write cost is what separates a Firestore database that scales gracefully from one you'll be rewriting six months from now.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
