Firestore Data Model
Understanding how Firestore organizes data is the foundation of building a well-performing app. A good data model makes queries fast and keeps costs low. A poor data model creates slow queries, unnecessary reads, and complex workarounds. This topic explains the data model clearly and shows how to structure common types of app data.
Collections, Documents, and Subcollections
Firestore has three levels of data organization:
Firestore
|
+-- Collection: "users"
| |
| +-- Document: "uid_alice"
| | { name: "Alice", age: 28 }
| | |
| | +-- Subcollection: "orders"
| | |
| | +-- Document: "order_001"
| | { item: "Laptop", price: 45000 }
| |
| +-- Document: "uid_bob"
| { name: "Bob", age: 34 }
|
+-- Collection: "products"
|
+-- Document: "prod_001"
{ title: "Laptop", stock: 12 }
Collection
A collection is a group of documents. Collections cannot directly contain data — only documents. Collection names are strings you define. Common examples: users, posts, orders, products.
Document
A document lives inside a collection and holds the actual data as field-value pairs. Every document has a unique ID within its collection — either assigned automatically by Firestore or set by you. Document IDs are strings. Maximum document size: 1 MB.
Subcollection
A subcollection is a collection nested inside a document. It groups related documents under a parent. For example, a user document can have an orders subcollection containing all orders that user placed.
Document Paths
Every document has a path that identifies its location in the database. Paths alternate between collection names and document IDs:
users/uid_alice (collection/document) users/uid_alice/orders/order_001 (collection/document/subcollection/document) products/prod_001 (collection/document)
This path structure matters in code. You reference a document by building its path:
import { doc } from "firebase/firestore";
import { db } from "./firebase";
// Reference to a user document
const userRef = doc(db, "users", "uid_alice");
// Reference to a nested order document
const orderRef = doc(db, "users", "uid_alice", "orders", "order_001");
Designing Data for a Blog App
Consider a blogging platform with users, posts, and comments. Here is a well-structured Firestore model:
Collection: "users"
Document: "uid_alice"
{
displayName: "Alice",
email: "alice@example.com",
photoURL: "https://...",
createdAt: Timestamp
}
Collection: "posts"
Document: "post_001"
{
title: "My First Post",
body: "Hello, world...",
authorId: "uid_alice",
authorName: "Alice", <-- duplicated for display speed
publishedAt: Timestamp,
tags: ["tech", "firebase"],
likesCount: 42
}
Subcollection: "comments"
Document: "comment_001"
{
text: "Great post!",
authorId: "uid_bob",
authorName: "Bob",
createdAt: Timestamp
}
Notice that authorName appears inside each post document even though the name also exists in the user document. This is intentional — a technique called denormalization.
Denormalization: Duplicating Data on Purpose
In a traditional SQL database, you store data once and join tables when you need it. In Firestore, joins do not exist. To display a post with the author's name, you either read the post document and the user document separately (two reads) or store the author's name directly in the post document (one read).
Firestore charges per read. Storing frequently needed data in the document you are already reading reduces extra reads and keeps your app fast.
Without denormalization: Read post document --> get authorId Read user document --> get authorName = 2 reads per post display With denormalization: Read post document --> get authorName directly = 1 read per post display
The tradeoff: if Alice changes her display name, you must update it in every post document she authored. Handle this with a Cloud Function that triggers when the user document updates.
Array vs Subcollection: When to Use Which
Small, fixed-length lists of simple values fit inside arrays in a document. Larger, growing lists of complex objects belong in subcollections.
Good use of array (small, simple list):
{
tags: ["firebase", "backend", "tutorial"],
favoriteColors: ["blue", "green"]
}
Good use of subcollection (many complex items):
posts/{postId}/comments/{commentId}
{
text: "Great article",
authorId: "uid_bob",
createdAt: Timestamp,
likes: 5
}
Arrays in Firestore cannot exceed the 1 MB document limit. Subcollections have no practical size limit and can be paginated and queried independently.
Flat Structure vs Nested Structure
Firestore works best with relatively flat structures. Avoid nesting collections many levels deep. Two levels of nesting is usually the maximum you need.
Good (flat):
users/{uid}
posts/{postId}
comments/{commentId} <-- top-level, store postId as a field
Less ideal (deep nesting):
users/{uid}/posts/{postId}/comments/{commentId}/replies/{replyId}
Top-level collections are easier to query across all users. Deep nesting makes cross-collection queries impossible — you can only query within a specific parent document's subcollection.
Using Firestore Document IDs
Firestore generates document IDs automatically when you add a document without specifying an ID. These auto-generated IDs are 20-character alphanumeric strings and are globally unique.
Sometimes using a meaningful ID is better:
- Use the user's UID as the document ID in a
userscollection — easy to look up a specific user - Use the product's SKU or slug as the ID in a
productscollection — avoids duplicate entries - Use auto-generated IDs for posts, orders, messages — you don't know them in advance
// Document with user's UID as ID
import { doc, setDoc } from "firebase/firestore";
await setDoc(doc(db, "users", user.uid), {
displayName: user.displayName,
email: user.email
});
// Now users/uid_alice exists and you can always retrieve it by uid
Key Takeaway
Firestore organizes data in collections of documents, with optional subcollections inside documents. Every document has a unique path like collection/docId or collection/docId/subcollection/docId. Design your data around what your queries need — denormalize data to reduce reads, use subcollections for growing lists of complex items, and keep your nesting shallow. Good data modeling decisions made early save significant time and cost later.
