Back to Diary
Technical

Optimizing Firestore - The Move to Subcollections

Bjarne
6 min read

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:

Old Structure
users/{userId}
userBooks/{bookId}        ← with a 'userId' field
readingSessions/{sessionId}  ← with a 'userId' field

To find a user's books, we had to perform a query:

typescript
collection('userBooks').where('userId', isEqualTo: currentUser.uid)

The problems

While this works, it has downsides. Security rules become complex because you have to check the 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:

New Structure
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
New subcollection architecture

Why This is Better

  1. Data Locality & Organization – All data belonging to a user is logically grouped together. It's cleaner and easier to reason about.
  2. Simplified Security Rules – We no longer need to inspect document fields to verify ownership. The path itself proves ownership.
  3. Streamlined Queries – We don't need to filter by userId anymore. We just query the subcollection directly.
  4. 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

The path itself proves ownership. If you can access /users/abc123/books/, you must be user abc123.

Here's what the security rules look like now:

firestore.rules
1match /users/{userId}/books/{bookId} {
2 // The path proves ownership - no field checking needed
3 allow read, write: if request.auth.uid == userId;
4}
5
6match /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.

user_book_repository.dart
1class UserBookRepository {
2 final String userId;
3
4 UserBookRepository(this.userId);
5
6 CollectionReference get booksCollection =>
7 FirebaseFirestore.instance
8 .collection('users')
9 .doc(userId)
10 .collection('books');
11
12 Future<List<UserBook>> getBooks() async {
13 final snapshot = await booksCollection.get();
14 return snapshot.docs
15 .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
firebasefirestorearchitectureflutter
B
Bjarne
Building Booklynx with love for readers everywhere