Designing a Firestore database from scratch is hard. You need to think about queries, denormalization, subcollections, and a dozen other NoSQL-specific concerns before writing your first line of code. Instead of starting from zero, use these 10 proven patterns as templates. Each one represents a common app architecture — e-commerce, chat, multi-tenant SaaS, location-based services — with a complete JSON Schema showing exactly how to model the collections. Copy the pattern that fits your use case, adapt it to your needs, and you'll have a solid foundation in minutes instead of weeks.
1. User Profiles
Social apps, SaaS platforms, membership sites
Collection Structure
firestore_structures.pattern1_structure
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"collection": "users",
"description": "User profiles with authentication info",
"schema": {
"type": "object",
"properties": {
"displayName": {
"type": "string",
"description": "User's full name"
},
"email": {
"type": "string",
"format": "email",
"description": "Login email address"
},
"photoURL": {
"type": "string",
"format": "uri",
"description": "Profile picture URL"
},
"role": {
"type": "string",
"enum": ["admin", "editor", "viewer"],
"description": "Access control role"
},
"createdAt": {
"type": "string",
"format": "date-time",
"description": "Account creation timestamp"
}
},
"required": ["displayName", "email", "role", "createdAt"]
}
}Why this design: Store user profiles as root-level documents with the Firebase Auth UID as the document ID. Denormalize frequently-accessed fields like displayName and photoURL so they can be embedded in other collections without extra reads.
Common mistake: Don't nest user settings or preferences as subcollections unless they have hundreds of items. Keep the user document lean and fast to read.
2. E-Commerce Products & Orders
Online stores, marketplaces, inventory systems
Collection Structure
firestore_structures.pattern2_structure
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"collection": "orders",
"description": "Customer purchase orders",
"schema": {
"type": "object",
"properties": {
"userId": {
"type": "string",
"description": "Reference to users collection"
},
"items": {
"type": "array",
"description": "Ordered items with denormalized product data",
"items": {
"type": "object",
"properties": {
"productId": { "type": "string" },
"name": { "type": "string" },
"price": { "type": "number", "minimum": 0 },
"quantity": { "type": "integer", "minimum": 1 }
},
"required": ["productId", "name", "price", "quantity"]
}
},
"total": {
"type": "number",
"minimum": 0,
"description": "Order total in USD"
},
"status": {
"type": "string",
"enum": ["pending", "confirmed", "shipped", "delivered"],
"description": "Current order status"
},
"createdAt": {
"type": "string",
"format": "date-time",
"description": "Order timestamp"
}
},
"required": ["userId", "items", "total", "status", "createdAt"]
}
}Why this design: Store orders as root-level documents with userId for filtering. Denormalize product name and price into order items so historical orders remain accurate even if product data changes later.
Common mistake: Don't store inventory counts in product documents — use a separate inventory collection or Cloud Functions to prevent race conditions on popular products.
3. Chat & Messaging
Real-time chat apps, customer support, collaboration tools
Collection Structure
firestore_structures.pattern3_structure
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"collection": "messages",
"description": "Chat messages within a room",
"schema": {
"type": "object",
"properties": {
"authorId": {
"type": "string",
"description": "User ID of message author"
},
"authorName": {
"type": "string",
"description": "Denormalized author display name"
},
"text": {
"type": "string",
"description": "Message content",
"maxLength": 5000
},
"createdAt": {
"type": "string",
"format": "date-time",
"description": "Message timestamp"
},
"edited": {
"type": "boolean",
"description": "Whether message was edited"
}
},
"required": ["authorId", "authorName", "text", "createdAt"]
}
}Why this design: Use subcollections for messages so each chat room can hold unlimited messages. Denormalize authorName to avoid fetching user profiles for every message display. Store lastMessage in the parent chatRoom for quick preview rendering.
Common mistake: Don't try to implement read receipts or typing indicators in Firestore documents — use Realtime Database or Firebase Cloud Messaging for ephemeral presence data.
4. Blog with Comments
Content platforms, CMS, community forums
Collection Structure
firestore_structures.pattern4_structure
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"collection": "posts",
"description": "Blog posts with metadata",
"schema": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Post title",
"maxLength": 200
},
"content": {
"type": "string",
"description": "Post body in markdown"
},
"authorId": {
"type": "string",
"description": "Reference to users collection"
},
"authorName": {
"type": "string",
"description": "Denormalized author name"
},
"publishedAt": {
"type": "string",
"format": "date-time",
"description": "Publication timestamp"
},
"commentCount": {
"type": "integer",
"minimum": 0,
"description": "Cached comment count"
},
"tags": {
"type": "array",
"items": { "type": "string" },
"description": "Post tags for filtering"
}
},
"required": ["title", "content", "authorId", "authorName", "publishedAt"]
}
}Why this design: Store posts as root documents with denormalized author info for efficient list rendering. Keep a cached commentCount updated via Cloud Functions to avoid reading the entire comments subcollection just to show a count.
Common mistake: Don't store the full comment list in the post document as an array — it breaks once you have more than a few dozen comments and makes pagination impossible.
5. Multi-Tenant SaaS
B2B applications, workspace-based tools, team management
Collection Structure
firestore_structures.pattern5_structure
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"collection": "organizations",
"description": "Tenant organizations for multi-tenant SaaS",
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Organization name"
},
"plan": {
"type": "string",
"enum": ["free", "pro", "enterprise"],
"description": "Subscription plan"
},
"ownerId": {
"type": "string",
"description": "User ID of organization owner"
},
"createdAt": {
"type": "string",
"format": "date-time",
"description": "Organization creation timestamp"
},
"memberCount": {
"type": "integer",
"minimum": 1,
"description": "Cached member count"
}
},
"required": ["name", "plan", "ownerId", "createdAt"]
}
}Why this design: Use organizations as the top-level collection with all tenant data nested beneath. This makes security rules simple: users can only access data within their organization. Store member roles in a subcollection for easy enumeration.
Common mistake: Don't duplicate organization data into user documents — it creates sync issues. Instead, query the organizations collection filtered by user membership.
6. Activity & Audit Log
Compliance tracking, user activity feeds, admin dashboards
Collection Structure
firestore_structures.pattern6_structure
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"collection": "activityLog",
"description": "Audit trail of user actions",
"schema": {
"type": "object",
"properties": {
"userId": {
"type": "string",
"description": "User who performed the action"
},
"userName": {
"type": "string",
"description": "Denormalized user name"
},
"action": {
"type": "string",
"enum": ["created", "updated", "deleted", "viewed"],
"description": "Action type"
},
"resourceType": {
"type": "string",
"description": "Type of resource affected (e.g., 'post', 'user')"
},
"resourceId": {
"type": "string",
"description": "ID of affected resource"
},
"metadata": {
"type": "object",
"description": "Additional context data"
},
"timestamp": {
"type": "string",
"format": "date-time",
"description": "When the action occurred"
}
},
"required": ["userId", "action", "resourceType", "resourceId", "timestamp"]
}
}Why this design: Store activity logs as a root collection with indexed fields for userId, action, and timestamp. Generate log entries via Cloud Functions on document writes to ensure complete audit trails even if client code fails.
Common mistake: Don't try to store logs as subcollections under users or resources — it makes cross-user or cross-resource queries impossible. Use a flat collection with reference fields.
7. Geolocation & Maps
Location-based services, delivery apps, store finders
Collection Structure
firestore_structures.pattern7_structure
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"collection": "locations",
"description": "Locations with geospatial data",
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Location name"
},
"address": {
"type": "string",
"description": "Street address"
},
"geopoint": {
"type": "object",
"description": "Firestore GeoPoint",
"properties": {
"latitude": { "type": "number", "minimum": -90, "maximum": 90 },
"longitude": { "type": "number", "minimum": -180, "maximum": 180 }
},
"required": ["latitude", "longitude"]
},
"geohash": {
"type": "string",
"description": "Geohash for proximity queries"
},
"category": {
"type": "string",
"enum": ["restaurant", "store", "office", "other"],
"description": "Location type"
}
},
"required": ["name", "geopoint", "geohash"]
}
}Why this design: Store geohash strings alongside GeoPoint fields to enable efficient proximity queries. Firestore can't do native radius searches, so geohash lets you filter by prefix to find nearby locations. Use libraries like geofire-common to generate hashes.
Common mistake: Don't try to query by latitude/longitude ranges directly — Firestore can't do compound inequality queries. Always use geohash prefixes for location-based filtering.
8. Inventory Management
Warehouse systems, stock tracking, supply chain apps
Collection Structure
firestore_structures.pattern8_structure
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"collection": "inventory",
"description": "Inventory items with stock levels",
"schema": {
"type": "object",
"properties": {
"sku": {
"type": "string",
"description": "Stock keeping unit code"
},
"name": {
"type": "string",
"description": "Item name"
},
"quantity": {
"type": "integer",
"minimum": 0,
"description": "Current stock quantity"
},
"warehouseId": {
"type": "string",
"description": "Warehouse location reference"
},
"reorderLevel": {
"type": "integer",
"minimum": 0,
"description": "Quantity threshold for reorder alerts"
},
"lastRestocked": {
"type": "string",
"format": "date-time",
"description": "Last restock timestamp"
}
},
"required": ["sku", "name", "quantity", "warehouseId"]
}
}Why this design: Use transactions when updating quantity to prevent race conditions. For high-volume inventory with frequent updates, consider using distributed counters or a separate transaction log collection to avoid write contention.
Common mistake: Don't increment/decrement quantity fields from client code without transactions — you'll get incorrect counts under concurrent access. Use Cloud Functions or server-side logic.
9. Social Feed & Followers
Social networks, news feeds, activity streams
Collection Structure
firestore_structures.pattern9_structure
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"collection": "posts",
"description": "Social feed posts",
"schema": {
"type": "object",
"properties": {
"authorId": {
"type": "string",
"description": "User ID of post author"
},
"authorName": {
"type": "string",
"description": "Denormalized author display name"
},
"content": {
"type": "string",
"description": "Post content",
"maxLength": 5000
},
"likeCount": {
"type": "integer",
"minimum": 0,
"description": "Cached like count"
},
"createdAt": {
"type": "string",
"format": "date-time",
"description": "Post timestamp"
}
},
"required": ["authorId", "authorName", "content", "createdAt"]
}
}Why this design: Store followers and following as separate subcollections under each user to enable efficient queries in both directions. Keep a cached likeCount on posts updated via Cloud Functions instead of reading the entire likes subcollection.
Common mistake: Don't fan out posts to all followers' feeds on write — it doesn't scale. Instead, query posts from followed users in real-time using an array-contains query on following IDs.
10. IoT & Sensor Data
IoT dashboards, environmental monitoring, device telemetry
Collection Structure
firestore_structures.pattern10_structure
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"collection": "readings",
"description": "Sensor readings from IoT devices",
"schema": {
"type": "object",
"properties": {
"temperature": {
"type": "number",
"description": "Temperature in Celsius"
},
"humidity": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Relative humidity percentage"
},
"batteryLevel": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Battery percentage"
},
"timestamp": {
"type": "string",
"format": "date-time",
"description": "Reading timestamp"
}
},
"required": ["timestamp"]
}
}Why this design: Store time-series data as subcollections under each device. For high-frequency data, batch writes and implement TTL cleanup via Cloud Functions to avoid unbounded growth. Consider Cloud Firestore TTL policies or BigQuery exports for long-term analytics.
Common mistake: Don't store every sensor reading if your device reports every second — you'll exceed Firestore's write limits and costs. Aggregate to minute or hour intervals, or use Realtime Database for high-frequency writes.
Turn These Patterns Into Documentation
These JSON Schemas aren't just examples — they're working documentation you can use in your project. Save them as .schema.json files in your repository, render them with FireSchema, and give your team a browsable reference they'll actually use. It takes 5 minutes to set up.
Next Steps
Now that you have patterns to work with, learn how to use them effectively: