Optimizing Firestore - The Move to Subcollections
As we build Booklynx, we're constantly evaluating our architecture to ensure it scales. One of our recent major technical shifts was refactoring how we store user data in Cloud Firestore.
The Old Way: Root Collections
Initially, our data model looked like this:
users/{userId}
userBooks/{bookId} ← with a 'userId' field
readingSessions/{sessionId} ← with a 'userId' fieldTo find a user's books, we had to perform a query:
collection('userBooks').where('userId', isEqualTo: currentUser.uid)The problems
userId field on every document access. Deleting a user meant hunting down documents across multiple root collections.The New Way: Subcollections
We've restructured the data to nest user-specific data directly under the user document:
users/{userId}
└── books/{bookId}
└── readingSessions/{sessionId}
└── libraries/{libraryId}User Document
- • Profile
- • Settings
- • Preferences
Books Subcollection
- • Your library
- • Reading progress
- • Notes
Sessions Subcollection
- • Reading sessions
- • Time tracking
- • Statistics
Libraries Subcollection
- • Custom shelves
- • Organization
- • Tags
Why This is Better
- Data Locality & Organization – All data belonging to a user is logically grouped together. It's cleaner and easier to reason about.
- Simplified Security Rules – We no longer need to inspect document fields to verify ownership. The path itself proves ownership.
- Streamlined Queries – We don't need to filter by userId anymore. We just query the subcollection directly.
- Easier Account Deletion – When a user decides to leave, we can easily identify and cascade delete their subcollections without scanning the entire database.
Security made simple
/users/abc123/books/, you must be user abc123.Here's what the security rules look like now:
1match /users/{userId}/books/{bookId} {2 // The path proves ownership - no field checking needed3 allow read, write: if request.auth.uid == userId;4}56match /users/{userId}/readingSessions/{sessionId} {7 allow read, write: if request.auth.uid == userId;8}The Migration
Refactoring this in a live app (even in development) required updating our repositories and Riverpod providers. We updated our UserBookRepositoryto accept a userId and construct paths dynamically.
1class UserBookRepository {2 final String userId;3 4 UserBookRepository(this.userId);5 6 CollectionReference get booksCollection =>7 FirebaseFirestore.instance8 .collection('users')9 .doc(userId)10 .collection('books');11 12 Future<List<UserBook>> getBooks() async {13 final snapshot = await booksCollection.get();14 return snapshot.docs15 .map((doc) => UserBook.fromFirestore(doc))16 .toList();17 }18}This change sets a solid foundation for Booklynx as we grow. It ensures that your library loads fast and stays secure.
Experience the speed
Join the beta and see how fast your library can load with our optimized architecture.
Join the Beta