I’m trying to build an app with content blocks that work like Notion does. Users can create different types of blocks and nest them inside each other. The tricky part is that blocks can contain other blocks, and those can contain more blocks, so it goes pretty deep.
I’m using Firestore and wondering what’s the smartest way to organize this data. Should I make each block store IDs that point to other blocks? Or should I just put all the nested data directly inside each block?
The reference approach seems cleaner but I’m worried about having to make tons of database calls to load everything. The nested data approach might be faster but could get messy with really deep nesting.
Built a similar collaborative workspace app - definitely go with references despite the performance worries. Firestore’s caching and batching handle the database calls better than you’d think. References saved my sanity when users started moving blocks around and sharing them between docs. No messy data sync headaches. I just made a simple block loader that batches fetches and caches locally. Performance hit was tiny - maybe 100-200ms on initial load - but maintaining the code became way easier. Pro tip: add a depth field to each block and set limits. You’ll thank me when you avoid infinite recursion crashes.
i’d store refs and preload smartly. firestore handles 500 batch reads, so performance won’t kill you. when someone opens a doc, grab all block ids at once and cache them. much easier than managing deeply nested objects that turn into update nightmares.
I faced this exact decision building a similar document editor last year. Tried both approaches and ended up with a hybrid that beats either pure solution. Here’s what worked: store immediate children directly in each block document, but only one level deep. Anything deeper gets stored as references. You get the performance wins for common cases without the data structure becoming a nightmare. The key insight? Most users don’t actually nest more than 2-3 levels deep, even though the system allows infinite nesting. So you get fast loading for 90% of cases without the complexity explosion. I added lazy loading for deeper references - they only fetch when users actually expand those sections. Works great with Firestore’s real-time listeners since you can subscribe to just the visible parts.