Subcollections allow you to organize data hierarchically within Firestore documents. FirestoreORM makes working with subcollections intuitive and type-safe.
Understanding Subcollections
Subcollections are collections that exist under a specific document. They enable hierarchical data organization.
Document Path Structure
collection/document/subcollection/subdocument
For example:
users/user-123/orders/order-456
posts/post-789/comments/comment-101
organizations/org-1/teams/team-2/members/member-3
Each document can have multiple subcollections, and subcollections can be nested indefinitely.
Creating Subcollection Repositories
Basic Subcollection Access
Use the subcollection() method to access a subcollection under a specific parent document.
// Access orders for a specific user
const userOrders = userRepo . subcollection < Order >(
'user-123' ,
'orders'
);
// Now use it like any repository
const order = await userOrders . create ({
product: 'Widget' ,
price: 99.99 ,
quantity: 2 ,
status: 'pending'
});
Subcollection with Schema Validation
Add Zod schema validation to subcollections.
import { z } from 'zod' ;
const orderSchema = z . object ({
id: z . string (). optional (),
product: z . string (),
price: z . number (). positive (),
quantity: z . number (). int (). positive (),
status: z . enum ([ 'pending' , 'completed' , 'cancelled' ])
});
const userOrders = userRepo . subcollection < Order >(
'user-123' ,
'orders' ,
orderSchema
);
// Validation happens automatically
await userOrders . create ({
product: 'Widget' ,
price: - 10 , // ❌ ValidationError: price must be positive
quantity: 2 ,
status: 'pending'
});
CRUD Operations in Subcollections
Create Documents
const userOrders = userRepo . subcollection < Order >( 'user-123' , 'orders' );
const order = await userOrders . create ({
product: 'Gaming Laptop' ,
price: 1299.99 ,
quantity: 1 ,
status: 'pending' ,
createdAt: new Date (). toISOString ()
});
console . log ( order . id ); // Auto-generated ID
// Full path: users/user-123/orders/xyz789
Read Documents
const userOrders = userRepo . subcollection < Order >( 'user-123' , 'orders' );
// Get by ID
const order = await userOrders . getById ( 'order-456' );
// List all orders for this user
const allOrders = await userOrders . list ( 50 );
// Query orders
const completedOrders = await userOrders . query ()
. where ( 'status' , '==' , 'completed' )
. orderBy ( 'createdAt' , 'desc' )
. get ();
Update Documents
const userOrders = userRepo . subcollection < Order >( 'user-123' , 'orders' );
await userOrders . update ( 'order-456' , {
status: 'shipped' ,
shippedAt: new Date (). toISOString ()
});
Delete Documents
const userOrders = userRepo . subcollection < Order >( 'user-123' , 'orders' );
// Soft delete
await userOrders . softDelete ( 'order-456' );
// Hard delete
await userOrders . delete ( 'order-456' );
Nested Subcollections
Subcollections can be nested to create complex hierarchies.
Multiple Levels
// posts/post-123/comments/comment-456/replies/reply-789
const postComments = postRepo . subcollection < Comment >(
'post-123' ,
'comments'
);
const commentReplies = postComments . subcollection < Reply >(
'comment-456' ,
'replies'
);
const reply = await commentReplies . create ({
author: 'user-999' ,
text: 'Great point!' ,
createdAt: new Date (). toISOString ()
});
Deep Nesting Example
// organizations/org-1/teams/team-2/projects/project-3/tasks/task-4
const orgTeams = organizationRepo . subcollection < Team >( 'org-1' , 'teams' );
const teamProjects = orgTeams . subcollection < Project >( 'team-2' , 'projects' );
const projectTasks = teamProjects . subcollection < Task >( 'project-3' , 'tasks' );
const task = await projectTasks . create ({
title: 'Implement feature X' ,
assignee: 'user-123' ,
status: 'in-progress'
});
While Firestore supports unlimited nesting, deep hierarchies can make queries complex. Consider flattening your data structure if you find yourself going more than 3 levels deep.
Querying Subcollections
Query Single User’s Orders
const userOrders = userRepo . subcollection < Order >( 'user-123' , 'orders' );
const recentOrders = await userOrders . query ()
. where ( 'status' , '==' , 'completed' )
. orderBy ( 'createdAt' , 'desc' )
. limit ( 10 )
. get ();
const userOrders = userRepo . subcollection < Order >( 'user-123' , 'orders' );
const { items , nextCursorId } = await userOrders . query ()
. orderBy ( 'createdAt' , 'desc' )
. paginate ( 20 );
console . log ( `Found ${ items . length } orders` );
Aggregations
const userOrders = userRepo . subcollection < Order >( 'user-123' , 'orders' );
// Total spent by user
const totalSpent = await userOrders . query ()
. where ( 'status' , '==' , 'completed' )
. aggregate ( 'price' , 'sum' );
console . log ( `User total spent: $ ${ totalSpent } ` );
Get Parent ID
const userOrders = userRepo . subcollection < Order >( 'user-123' , 'orders' );
const parentId = userOrders . getParentId ();
console . log ( parentId ); // 'user-123'
Get Collection Path
const userOrders = userRepo . subcollection < Order >( 'user-123' , 'orders' );
const path = userOrders . getCollectionPath ();
console . log ( path ); // 'users/user-123/orders'
Check if Subcollection
const userOrders = userRepo . subcollection < Order >( 'user-123' , 'orders' );
console . log ( userOrders . isSubcollection ()); // true
const topLevel = new FirestoreRepository ( db , 'users' );
console . log ( topLevel . isSubcollection ()); // false
Real-World Examples
E-commerce User Orders
interface Order {
id ?: string ;
items : OrderItem [];
total : number ;
status : 'pending' | 'processing' | 'shipped' | 'delivered' ;
shippingAddress : Address ;
createdAt : string ;
updatedAt : string ;
}
class OrderService {
async createOrder ( userId : string , orderData : Omit < Order , 'id' >) {
const userOrders = userRepo . subcollection < Order >( userId , 'orders' );
return await userOrders . create ( orderData );
}
async getUserOrders ( userId : string , status ?: Order [ 'status' ]) {
const userOrders = userRepo . subcollection < Order >( userId , 'orders' );
let query = userOrders . query (). orderBy ( 'createdAt' , 'desc' );
if ( status ) {
query = query . where ( 'status' , '==' , status );
}
return await query . get ();
}
async updateOrderStatus (
userId : string ,
orderId : string ,
status : Order [ 'status' ]
) {
const userOrders = userRepo . subcollection < Order >( userId , 'orders' );
return await userOrders . update ( orderId , {
status ,
updatedAt: new Date (). toISOString ()
});
}
}
interface Comment {
id ?: string ;
authorId : string ;
authorName : string ;
text : string ;
likes : number ;
createdAt : string ;
updatedAt : string ;
}
class CommentService {
async addComment (
postId : string ,
authorId : string ,
text : string
) {
const postComments = postRepo . subcollection < Comment >( postId , 'comments' );
// Get author info
const author = await userRepo . getById ( authorId );
if ( ! author ) throw new Error ( 'Author not found' );
return await postComments . create ({
authorId ,
authorName: author . name ,
text ,
likes: 0 ,
createdAt: new Date (). toISOString (),
updatedAt: new Date (). toISOString ()
});
}
async getComments ( postId : string , limit : number = 50 ) {
const postComments = postRepo . subcollection < Comment >( postId , 'comments' );
return await postComments . query ()
. orderBy ( 'createdAt' , 'desc' )
. limit ( limit )
. get ();
}
async likeComment ( postId : string , commentId : string ) {
const postComments = postRepo . subcollection < Comment >( postId , 'comments' );
const comment = await postComments . getById ( commentId );
if ( ! comment ) throw new Error ( 'Comment not found' );
return await postComments . update ( commentId , {
likes: comment . likes + 1
});
}
}
Organization Structure
interface Team {
id ?: string ;
name : string ;
description : string ;
memberCount : number ;
createdAt : string ;
}
interface Member {
id ?: string ;
userId : string ;
role : 'owner' | 'admin' | 'member' ;
joinedAt : string ;
}
class OrganizationService {
async addTeam ( orgId : string , teamData : Omit < Team , 'id' >) {
const orgTeams = organizationRepo . subcollection < Team >( orgId , 'teams' );
return await orgTeams . create ( teamData );
}
async addTeamMember (
orgId : string ,
teamId : string ,
userId : string ,
role : Member [ 'role' ]
) {
const orgTeams = organizationRepo . subcollection < Team >( orgId , 'teams' );
const teamMembers = orgTeams . subcollection < Member >( teamId , 'members' );
const member = await teamMembers . create ({
userId ,
role ,
joinedAt: new Date (). toISOString ()
});
// Update member count
const team = await orgTeams . getById ( teamId );
if ( team ) {
await orgTeams . update ( teamId , {
memberCount: team . memberCount + 1
});
}
return member ;
}
async getTeamMembers ( orgId : string , teamId : string ) {
const orgTeams = organizationRepo . subcollection < Team >( orgId , 'teams' );
const teamMembers = orgTeams . subcollection < Member >( teamId , 'members' );
return await teamMembers . query ()
. orderBy ( 'joinedAt' , 'asc' )
. get ();
}
}
Bulk Operations in Subcollections
Bulk Create
const userOrders = userRepo . subcollection < Order >( 'user-123' , 'orders' );
const orders = await userOrders . bulkCreate ([
{ product: 'Item 1' , price: 10 , quantity: 1 , status: 'pending' },
{ product: 'Item 2' , price: 20 , quantity: 2 , status: 'pending' },
{ product: 'Item 3' , price: 30 , quantity: 1 , status: 'pending' }
]);
Bulk Update
const userOrders = userRepo . subcollection < Order >( 'user-123' , 'orders' );
await userOrders . bulkUpdate ([
{ id: 'order-1' , data: { status: 'shipped' } },
{ id: 'order-2' , data: { status: 'shipped' } },
{ id: 'order-3' , data: { status: 'shipped' } }
]);
Query Update
const userOrders = userRepo . subcollection < Order >( 'user-123' , 'orders' );
// Mark all pending orders as processing
const count = await userOrders . query ()
. where ( 'status' , '==' , 'pending' )
. update ({ status: 'processing' });
console . log ( `Updated ${ count } orders` );
Subcollections with Hooks
Lifecycle hooks work the same way in subcollections.
const userOrders = userRepo . subcollection < Order >( 'user-123' , 'orders' );
// Add hook to subcollection
userOrders . on ( 'afterCreate' , async ( order ) => {
console . log ( `Order ${ order . id } created for user-123` );
await sendOrderConfirmation ( 'user-123' , order );
});
const order = await userOrders . create ({
product: 'Widget' ,
price: 99.99 ,
quantity: 1 ,
status: 'pending'
});
// Hook executes automatically
Hooks are instance-specific. Each subcollection repository has its own hooks, separate from the parent repository.
Best Practices
Use Subcollections for 1-to-Many Relationships
Perfect for orders under users, comments under posts, etc. // ✅ Good - natural hierarchy
const userOrders = userRepo . subcollection ( 'user-123' , 'orders' );
// ❌ Less optimal - flat structure
const orders = await orderRepo . query ()
. where ( 'userId' , '==' , 'user-123' )
. get ();
Keep Hierarchies Shallow
Avoid going more than 2-3 levels deep. // ✅ Good - 2 levels
users / user - 123 / orders / order - 456
// ⚠️ Consider flattening - 4 levels
orgs / org - 1 / teams / team - 2 / projects / proj - 3 / tasks / task - 4
Don't Query Across All Subcollections
Firestore can’t efficiently query all subcollections of the same name across different parents. // ❌ Can't do this efficiently:
// "Get all orders from all users"
// ✅ Instead, maintain a top-level collection:
const allOrders = await ordersRepo . query (). get ();
Clean Up Subcollections on Parent Delete
Deleting a parent document doesn’t delete its subcollections. userRepo . on ( 'beforeDelete' , async ( user ) => {
// Delete user's orders
const userOrders = userRepo . subcollection ( 'orders' , user . id );
const orders = await userOrders . query (). get ();
await userOrders . bulkDelete ( orders . map ( o => o . id ));
});
When NOT to Use Subcollections
Avoid subcollections when:
You need to query data across multiple parents
The relationship is many-to-many
You need to access the data frequently without the parent ID
The subcollection could grow unbounded (use top-level collection instead)
Use Top-Level Collection Instead
// ❌ Hard to query all products across all categories
const categoryProducts = categoryRepo . subcollection ( 'electronics' , 'products' );
// ✅ Better - top-level products with category reference
const allProducts = await productRepo . query ()
. where ( 'category' , '==' , 'electronics' )
. get ();
Next Steps
CRUD Operations Master basic operations that work in subcollections
Queries Build complex queries in subcollections
Transactions Use transactions across parent and subcollection documents
Data Modeling Learn when to use subcollections vs top-level collections