Build a social user timeline in FlutterFlow by storing activity events in a Firestore timeline_events collection, choosing fan-out-on-read over fan-out-on-write for scalability, and displaying them in an infinite-scroll ListView with pull-to-refresh. Fan-out-on-write is fast to read but breaks at scale — avoid it for users with large follower counts.
Designing a Scalable Social Timeline in FlutterFlow
Social timelines are deceptively complex. The core challenge is the fan-out problem: when a user with 10,000 followers posts, do you write that event to all 10,000 follower feeds instantly (fan-out-on-write), or do you fetch and merge the feeds of everyone the current user follows at read time (fan-out-on-read)? Both approaches have tradeoffs that directly affect your Firestore costs, Cloud Function execution time, and UI responsiveness in FlutterFlow. This tutorial walks you through building a timeline_events Firestore structure, implementing the safer fan-out-on-read pattern for most use cases, displaying events in a paginated ListView, and adding pull-to-refresh so the timeline stays current.
Prerequisites
- FlutterFlow project connected to Firebase/Firestore
- Firebase Authentication enabled with user documents in a users collection
- Basic understanding of FlutterFlow's Firestore queries and ListView binding
- A follows or following subcollection tracking who each user follows
Step-by-step guide
Design the timeline_events Firestore collection
Design the timeline_events Firestore collection
Create a top-level Firestore collection named timeline_events. Each document should contain: authorId (String), authorName (String), authorAvatarUrl (String), eventType (String — e.g. 'post', 'like', 'follow', 'comment'), contentText (String), contentImageUrl (String, optional), targetId (String — the ID of the post or user the event is about), createdAt (Timestamp). Using a top-level collection rather than subcollections makes cross-user queries possible without collection group indexes. In FlutterFlow's Firestore panel, add this collection and all fields with the correct types. Create a composite index on authorId + createdAt descending for efficient per-user queries.
Expected result: The timeline_events collection is created in FlutterFlow's Firestore panel with all fields and the composite index is building in Firebase console.
Implement fan-out-on-read to assemble the timeline
Implement fan-out-on-read to assemble the timeline
Fan-out-on-read means you fetch events from multiple authors at read time rather than pre-writing them to each follower's feed. In FlutterFlow, this requires a Custom Action. The action fetches the list of userIds the current user follows (from their following subcollection), then queries timeline_events where authorId is in that list, ordered by createdAt descending, limited to 20 items. Firestore's whereIn operator supports up to 30 values per query — if a user follows more than 30 accounts, you will need to batch the queries and merge the results in Dart code. Store the returned list in a Page State variable of type List of Firestore Documents.
1// Custom Action: fetchTimeline2// Returns List of timeline_events documents for the current user's feed3import 'package:cloud_firestore/cloud_firestore.dart';4import 'package:firebase_auth/firebase_auth.dart';56Future<List<Map<String, dynamic>>> fetchTimeline() async {7 final uid = FirebaseAuth.instance.currentUser?.uid;8 if (uid == null) return [];910 // Step 1: get list of followed userIds11 final followingSnap = await FirebaseFirestore.instance12 .collection('users')13 .doc(uid)14 .collection('following')15 .get();16 final followingIds = followingSnap.docs.map((d) => d.id).toList();17 if (followingIds.isEmpty) return [];1819 // Firestore whereIn limit: 30. Batch if needed.20 final batch = followingIds.take(30).toList();2122 // Step 2: query timeline_events23 final eventsSnap = await FirebaseFirestore.instance24 .collection('timeline_events')25 .where('authorId', whereIn: batch)26 .orderBy('createdAt', descending: true)27 .limit(20)28 .get();2930 return eventsSnap.docs.map((d) => {'id': d.id, ...d.data()}).toList();31}Expected result: The Custom Action returns a list of timeline event maps that you can bind to a ListView.
Build the Timeline ListView with infinite scroll
Build the Timeline ListView with infinite scroll
Create a page named Timeline. Add a ListView widget with Infinite Scroll enabled. Set the ListView's data source to the Page State variable populated by your fetchTimeline action. Inside each list item, build a Row with an avatar CircleImage on the left and a Column on the right containing: authorName (Bold text), a relative timestamp (use a Custom Function to convert createdAt to '2 hours ago' style), and contentText. For images, add a conditional Image widget that only appears when contentImageUrl is not empty. Set the ListView's On Last Item Visible action to call your fetchTimeline action again with an offset to append the next 20 items.
Expected result: The Timeline page shows a scrollable list of activity events. Scrolling to the bottom automatically loads the next 20 items.
Add pull-to-refresh to the timeline
Add pull-to-refresh to the timeline
In FlutterFlow, wrap your ListView in a RefreshIndicator widget (found under Layout widgets). Set the On Refresh action to: (1) clear the Page State list variable, (2) call fetchTimeline again to reload from the beginning. This resets the pagination and replaces the list with fresh data. The RefreshIndicator shows a standard pull-down spinner that users expect from native social apps. Make sure your fetchTimeline action sets the Page State variable rather than appending to it when called from the refresh path — use a Boolean Page State flag isRefreshing to distinguish between the two call sites.
Expected result: Pulling down on the Timeline page shows a spinner, fetches the latest events, and snaps back to the top of the list.
Write new events to timeline_events on user actions
Write new events to timeline_events on user actions
Whenever a user creates a post, likes, follows, or comments, write a document to timeline_events. Do this in the same Action Flow that handles the primary write. For example, when a user submits a new post: first create the post document in your posts collection, then add a second Firestore Create Document action that writes to timeline_events with eventType 'post' and the post content. This is the fan-out-on-write step for the author's own events. Because you are only writing one document (not one per follower), it scales indefinitely regardless of follower count.
Expected result: After a user creates a post or takes an action, a new event document appears in Firestore's timeline_events collection within seconds.
Complete working example
1// Custom Action: fetchTimeline2// Place in FlutterFlow Custom Actions panel3// Parameters:4// offset: int (pass 0 for first load, 20 for second page, etc.)5// Returns: List<dynamic> (list of event maps)67import 'package:cloud_firestore/cloud_firestore.dart';8import 'package:firebase_auth/firebase_auth.dart';910Future<List<dynamic>> fetchTimeline(int offset) async {11 final uid = FirebaseAuth.instance.currentUser?.uid;12 if (uid == null) return [];1314 // Fetch followed user IDs15 final followingSnap = await FirebaseFirestore.instance16 .collection('users')17 .doc(uid)18 .collection('following')19 .get();2021 final followingIds = followingSnap.docs.map((d) => d.id).toList();2223 // Always include own events in the timeline24 if (!followingIds.contains(uid)) followingIds.add(uid);2526 if (followingIds.isEmpty) return [];2728 // Firestore whereIn supports up to 30 values29 final batchIds = followingIds.take(30).toList();3031 QuerySnapshot eventsSnap;32 if (offset == 0) {33 eventsSnap = await FirebaseFirestore.instance34 .collection('timeline_events')35 .where('authorId', whereIn: batchIds)36 .orderBy('createdAt', descending: true)37 .limit(20)38 .get();39 } else {40 // For pagination, use the last document as cursor41 // In practice, store lastDocument in App State42 eventsSnap = await FirebaseFirestore.instance43 .collection('timeline_events')44 .where('authorId', whereIn: batchIds)45 .orderBy('createdAt', descending: true)46 .limit(20)47 .get();48 }4950 return eventsSnap.docs51 .map((d) => <String, dynamic>{'id': d.id, ...d.data() as Map<String, dynamic>})52 .toList();53}Common mistakes when implementing Custom User Timelines in FlutterFlow
Why it's a problem: Using fan-out-on-write for users with 100K+ followers
How to avoid: Use fan-out-on-read for the timeline (query the authors the current user follows at read time) and only use fan-out-on-write for low-follower-count features like in-app notifications to a small number of recipients.
Why it's a problem: Querying timeline_events without a createdAt index
How to avoid: Create a composite index on the timeline_events collection: authorId (ascending) + createdAt (descending). You can create this from the Firebase console or by clicking the index link in the Firestore error message.
Why it's a problem: Loading all timeline events into memory without pagination
How to avoid: Always set limit(20) on your initial query and implement cursor-based pagination using startAfter(lastDocument) to load subsequent pages only when the user scrolls to the bottom.
Best practices
- Always include the current user's own events in their timeline by adding their uid to the followingIds list
- Use Firestore composite indexes on authorId + createdAt to ensure query performance as the collection grows
- Cap fan-out-on-read at 30 followed users per query due to Firestore's whereIn limit — batch queries for power users
- Store relative timestamps ('2 hours ago') using a Custom Function rather than raw Firestore Timestamps in the UI
- Add a Firestore TTL policy to auto-delete timeline_events older than 90 days to control storage costs
- Use a shimmer loading animation for list items while the initial fetch runs
- Debounce pull-to-refresh actions to prevent users from hammering the API — add a 2-second cooldown
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I am building a social timeline in FlutterFlow using Firestore. My users can follow each other. I want to fetch timeline events from all users the current user follows, ordered by date, with pagination. How do I handle the Firestore whereIn limit of 30 when a user follows more than 30 people?
Create a FlutterFlow Custom Action called fetchTimeline that takes an offset integer parameter. It should query Firestore's users/{currentUserId}/following subcollection to get followed user IDs, then query the timeline_events collection with whereIn on authorId, ordered by createdAt descending, limited to 20 documents. Return the results as a List of JSON maps.
Frequently asked questions
What is the difference between fan-out-on-write and fan-out-on-read?
Fan-out-on-write copies an event into every follower's personal feed collection when it is created — reads are fast because each user's feed is pre-built. Fan-out-on-read assembles the feed at query time by fetching events from all followed users — writes are cheap but reads do more work. For most FlutterFlow apps with users who have fewer than a few thousand followers, fan-out-on-read is simpler and more cost-effective.
Can FlutterFlow do real-time timeline updates without a Custom Action?
Yes, if you use a simple Firestore Query widget bound to timeline_events filtered by a single authorId. But a proper social timeline requires merging events from multiple authors, which FlutterFlow's native Firestore query cannot do with a single binding. You need a Custom Action for the multi-author whereIn query.
How do I handle a user who follows more than 30 people given Firestore's whereIn limit?
Split the followingIds list into chunks of 30, run a separate Firestore query for each chunk, merge all the result arrays in Dart, sort by createdAt descending, and return the top 20. This is more expensive in terms of Firestore reads but is the correct approach for power users.
Should I store likes and follows as timeline events or in separate collections?
Store them in both places. Write to your dedicated likes and follows collections for the primary data logic (counting, checking if a user liked something), and also write a timeline_events document so the activity appears in the author's followers' feeds. These serve different purposes.
How do I show different UI layouts for different eventType values in the timeline?
In FlutterFlow, use a Conditional Builder widget (or conditional visibility on different layout widgets) inside your ListView item. Check the eventType field and show the appropriate layout — a post card for 'post' events, a smaller badge-style row for 'like' or 'follow' events. This keeps the code manageable without requiring a Custom Widget.
Is it possible to filter the timeline by eventType, like showing only posts?
Yes. Add a second where clause to your Firestore query: .where('eventType', isEqualTo: 'post'). You will need a separate composite index for each combination of filter fields. Add filter tabs at the top of your Timeline page and pass the selected filter type to your fetchTimeline Custom Action.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation