{}FireSchema

Firestore Best Practices: 15 Rules Every Developer Should Know

Data modeling, queries, security, and cost optimization for Firebase projects

Building with Firestore? These 15 rules will save you from the most common pitfalls. Whether you're starting your first Firebase project or scaling an existing one, these best practices cover everything from data modeling to cost optimization — learned from real production apps.

Data Modeling Rules

How you structure your data determines everything else — query performance, costs, and maintainability. Get this right first.

1. Prefer flat collections over deep nesting

Deeply nested subcollections make queries harder and create tight coupling. Keep your collection hierarchy shallow — usually 1-2 levels deep. Use root-level collections with document references instead of nesting everything under a parent. For example, store orders as a root collection with a userId field rather than nesting under users/{id}/orders — unless you only ever query orders by user.

2. Denormalize data for read performance

In SQL you normalize to avoid duplication. In Firestore, you denormalize to avoid multiple reads. If your UI shows a user's name next to every comment, store authorName directly in each comment document — don't force a separate read to the users collection. Accept the trade-off: writes are more complex, but reads are fast and cheap.

3. Use subcollections for unbounded one-to-many relationships

When a document can have hundreds or thousands of related items (messages in a chat, orders for a user), use subcollections. Unlike arrays inside documents, subcollections can hold unlimited items and support efficient queries with pagination. Example: chats/{chatId}/messages/{messageId}.

4. Keep documents under 20 KB for optimal performance

Firestore's hard limit is 1 MB per document, but aim for under 20 KB. Large documents waste bandwidth when you only need a few fields — and Firestore charges per document read regardless of size. If a document is growing, split it into subcollections or a separate collection.

5. Avoid arrays for queryable data

Arrays in Firestore have limitations: you can't update individual elements, array-contains queries only support one per query, and arrays don't scale well past a few hundred items. For tags or categories use array-contains, but for relationships or growing lists, use subcollections or map fields instead.

Query Optimization

Firestore queries are fast by design, but only if you work with its constraints — not against them.

6. Create composite indexes proactively

Firestore requires an index for every unique query pattern. Instead of waiting for error messages, plan your indexes upfront. Define them in firestore.indexes.json and deploy with firebase deploy --only firestore:indexes. Every where + orderBy combination needs its own composite index.

7. Use cursor-based pagination, not offsets

Never use offset-based pagination in Firestore — it still reads (and charges for) all skipped documents. Instead, use startAfter() with the last document from the previous page. This is both faster and cheaper, as Firestore only reads the documents you actually need.

8. Limit query results explicitly

Always add .limit() to your queries. Without it, Firestore returns every matching document — which could be millions. Even if you think a collection is small today, it won't be in six months. A good default is 20-50 documents per query.

9. Avoid reading entire collections

If you find yourself doing collection('users').get() without filters, your data model needs rethinking. Use aggregation queries (count(), sum()) for analytics, and always filter with where() for display queries. Reading all documents is the #1 cause of unexpected Firestore bills.

Security Rules Patterns

Firestore security rules are your only server-side validation. Get them wrong and your entire database is exposed.

10. Always validate field types in rules

Don't just check if a field exists — validate its type. A malicious client could send age: "not a number" if you only check request.resource.data.age != null. Use is string, is number, is bool to enforce types at the security rules level.

11. Use granular per-collection rules

Never use allow read, write: if true at the database level. Write specific rules for each collection: who can read, who can create, who can update, who can delete. Start with everything denied and open up access incrementally. Use match patterns for subcollections.

12. Never trust client-side data

The client can send any data it wants. Validate required fields, check data types, enforce enum values, and verify that reference fields point to real documents. Use request.auth to verify the user is who they claim to be, and resource.data to compare against existing values.

Cost Optimization

Firestore pricing is based on reads, writes, and storage. Small changes in how you query can have big impacts on your bill.

13. Batch writes to reduce operations

Use writeBatch() or runTransaction() to combine multiple writes into a single operation. A batch of 500 writes costs the same as 500 individual writes in terms of operations, but reduces network round trips and ensures atomicity. Limit batches to 500 operations maximum.

14. Cache frequently-read documents

Firestore's client SDK has built-in offline persistence — enable it. For server-side applications, implement a cache layer (Redis, in-memory) for documents that change infrequently but are read constantly, like app configuration or user profiles. Every cached read is a read you don't pay for.

15. Use aggregation queries instead of reading all docs

Need a count of documents? Use countQuery() instead of fetching all documents and counting client-side. Firestore aggregation queries (count(), sum(), average()) read index entries instead of full documents, costing roughly 1/1000th per entry.

Bonus: Document Your Database Structure

All these best practices are useless if your team can't find them. As your Firestore database grows, keeping track of collection structures, field types, and validation rules becomes a challenge. JSON Schema gives you a standard way to describe your database structure — and tools like FireSchema turn those schemas into interactive, browsable documentation your whole team can reference.

💡 Tip: Create a .schema.json file for each collection documenting its fields, types, and validation rules. This becomes your team's single source of truth.

Next Steps

Keep learning about Firestore and database documentation:

Try FireSchema

Quick Start Guide