Build a complete real-time chat application using Firestore conversations and messages subcollections. Display a conversations list with unread badges and last message preview. The chat screen uses a reversed ListView for real-time messages with sent/received bubble styling, image sharing via FlutterFlowUploadButton, and a pinned input bar. Online presence is tracked by updating a lastSeen timestamp on the user document every 60 seconds via a Timer.
Full chat application with conversations, real-time messages, and presence
This tutorial builds a complete peer-to-peer chat application: a conversations collection tracking participants and last message, a messages subcollection with real-time streaming, a conversations list page with unread badges, a chat page with styled message bubbles (sent right-aligned in blue, received left-aligned in grey), image sharing via Firebase Storage, and user presence tracking. This is a full APPLICATION — for a single embeddable chat widget, see the chat widget tutorial.
Prerequisites
- A FlutterFlow project with Firebase/Firestore connected
- Firebase Authentication enabled with at least two test user accounts
- Firebase Storage enabled for image message uploads
- Understanding of Backend Queries with real-time streams in FlutterFlow
Step-by-step guide
Design the Firestore conversations and messages data model
Design the Firestore conversations and messages data model
Create a conversations collection with fields: participants (List of Strings — user UIDs), lastMessage (String — preview text), lastMessageTime (Timestamp), and unreadCount (Map — keyed by userId, e.g., {uid1: 0, uid2: 3}). Create a subcollection conversations/{id}/messages with fields: senderId (String), text (String), type (String — text or image), imageUrl (String — empty for text messages), and timestamp (Timestamp). Set Firestore rules: allow read/write on conversations only if request.auth.uid is in participants array; allow read/write on messages if parent conversation's participants includes the user. Create a test conversation with a few messages between two test users.
Expected result: Firestore has conversations and messages subcollections with test data and security rules scoped to participants.
Build the conversations list page with unread badges
Build the conversations list page with unread badges
Create a ConversationsPage with a ListView bound to a Backend Query on conversations where participants arrayContains currentUser.uid, ordered by lastMessageTime descending, with Single Time Query OFF for real-time updates. Each list item is a Row with: a CircleImage showing the other participant's avatar (query the users collection with the other UID from participants list), a Column with the other user's displayName in bold and lastMessage preview in secondary text (maxLines: 1, overflow: ellipsis), a Text showing relative time from lastMessageTime, and a Badge Container (circular, red, 20x20) showing the unread count for the current user from the unreadCount map. Apply Conditional Visibility on the badge: hide when unreadCount[currentUser.uid] == 0. On tap, navigate to ChatPage passing the conversation document reference.
Expected result: A real-time list of conversations shows the other participant's avatar, name, last message, time, and unread count badge.
Create the chat page with reversed ListView and message bubbles
Create the chat page with reversed ListView and message bubbles
Create ChatPage that receives a conversationRef parameter. Add a Backend Query for the messages subcollection under conversationRef, ordered by timestamp descending (reversed list shows newest at bottom), Single Time Query OFF for real-time streaming. Use a reversed ListView so new messages appear at the bottom and the user can scroll up for history. Each message is a Container styled conditionally: if senderId == currentUser.uid, align right with blue background and white text (sent bubble); otherwise align left with grey background and dark text (received bubble). Add rounded corners (borderRadius: 16 on message side, 4 on tail side). Show the timestamp below each bubble in caption style. For image messages, show an Image widget instead of Text when type == image, with borderRadius clipping.
Expected result: Messages stream in real-time. Sent messages appear right-aligned in blue, received messages left-aligned in grey, with image messages rendered inline.
Add the message input bar with text and image sending
Add the message input bar with text and image sending
Pin a Row at the bottom of the ChatPage using a Column with the ListView in an Expanded and the input Row below. The Row contains: a FlutterFlowUploadButton (camera icon) configured for Firebase Storage uploads to chat_images/{conversationId}/{timestamp}.jpg, a TextField (expanded, hint: 'Type a message...', multiline: false), and an IconButton (Icons.send, primary color). On Send tap, the Action Flow creates a document in the messages subcollection with senderId: currentUser.uid, text: textFieldValue, type: text, imageUrl: empty, timestamp: serverTimestamp. Then update the parent conversation document: set lastMessage to the text, lastMessageTime to now, and increment the other user's unreadCount by 1 using FieldValue.increment. Clear the TextField after sending. For image upload: on FlutterFlowUploadButton complete, create a message with type: image, imageUrl: uploadedUrl, and text: 'Sent an image'.
Expected result: Users can send text messages and images. The conversation list updates with the latest message preview and unread count increments for the recipient.
Reset unread count on chat open and implement presence
Reset unread count on chat open and implement presence
On ChatPage load, add an On Page Load action that updates the conversation document: set unreadCount[currentUser.uid] to 0. This clears the badge when the user opens the chat. For online presence, update the user document's lastSeen field to Timestamp.now() on every page load across the app. Additionally, add a Periodic Action (Timer) on your main scaffold that runs every 60 seconds and updates lastSeen. On the ConversationsPage, display a green dot next to users whose lastSeen is within the last 2 minutes (Conditional Visibility: lastSeen > now - 120 seconds). Create a Custom Function isOnline that takes a Timestamp and returns true if it is within the last 120 seconds of DateTime.now().
Expected result: Opening a chat clears the unread badge. A green dot appears next to users who are currently active in the app.
Complete working example
1Firestore Data Model:2├── conversations/{conversationId}3│ ├── participants: List<String> (["uid_alice", "uid_bob"])4│ ├── lastMessage: String ("See you tomorrow!")5│ ├── lastMessageTime: Timestamp6│ └── unreadCount: Map ({"uid_alice": 0, "uid_bob": 2})7├── conversations/{id}/messages/{messageId}8│ ├── senderId: String ("uid_alice")9│ ├── text: String ("See you tomorrow!")10│ ├── type: String ("text" | "image")11│ ├── imageUrl: String ("" or storage URL)12│ └── timestamp: Timestamp13└── users/{uid}14 ├── displayName: String15 ├── avatarUrl: String16 └── lastSeen: Timestamp (presence)1718ConversationsPage:19├── AppBar: "Messages"20└── ListView (query: conversations where participants21│ arrayContains currentUser, orderBy lastMessageTime DESC,22│ real-time: ON)23 └── Row (per conversation)24 ├── Stack25 │ ├── CircleImage (other user avatar, 48x48)26 │ └── Positioned (green dot, 12x12) [Cond: isOnline]27 ├── Column (expanded)28 │ ├── Text (other user name, bold)29 │ └── Text (lastMessage, grey, maxLines: 1)30 ├── Column (end)31 │ ├── Text (relative time, caption)32 │ └── Container (red circle, unread count)33 │ └── Cond. Vis: unreadCount[myUid] > 034 └── On Tap → Navigate to ChatPage(conversationRef)3536ChatPage:37├── On Page Load → Update conversation: unreadCount[myUid] = 038├── ListView.reversed (query: messages, orderBy timestamp DESC,39│ real-time: ON)40│ └── Align (end if sent, start if received)41│ └── Container (blue if sent, grey if received,42│ borderRadius: 16)43│ ├── Text (message) OR Image (if type==image)44│ └── Text (timestamp, caption)45└── Row (pinned bottom)46 ├── FlutterFlowUploadButton (camera icon → Storage)47 ├── TextField (expanded, hint: 'Type a message...')48 └── IconButton (send) → Create message + Update conversationCommon mistakes when creating a Real-Time Chat Application in FlutterFlow
Why it's a problem: Using arrayContains query on participants without a Firestore composite index
How to avoid: Create a composite index in the Firebase Console: conversations collection, participants (arrayContains) + lastMessageTime (descending). Alternatively, run the query once and check the Firebase Console logs for the auto-generated index creation link.
Why it's a problem: Not using a reversed ListView for the chat messages
How to avoid: Order the messages query by timestamp descending and set the ListView to reversed mode. This shows the newest message at the bottom and lets users scroll up for older messages.
Why it's a problem: Querying the entire messages collection instead of the subcollection
How to avoid: Query the messages subcollection: conversations/{conversationId}/messages. Pass the conversationRef to the ChatPage and use it as the parent document for the subcollection Backend Query.
Best practices
- Use real-time queries (Single Time Query: OFF) for both conversations list and messages so updates appear instantly
- Store unreadCount as a map on the conversation document to avoid expensive subcollection count queries
- Use a reversed ListView ordered by timestamp descending for natural chat scrolling behavior
- Limit message queries to 50 with infinite scroll to avoid loading thousands of messages at once
- Update lastSeen for presence tracking every 60 seconds — more frequent updates waste Firestore writes
- Compress uploaded images before storing to Firebase Storage to reduce bandwidth and loading time
- Create the composite index for participants + lastMessageTime proactively before deploying
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Design a Firestore data model for a real-time chat application with conversations (participants, last message, unread counts) and a messages subcollection (sender, text, type, image URL, timestamp). Write the Firestore Security Rules that only allow conversation participants to read and write messages. Include the composite index definition needed for the conversations list query.
Create a chat page with a reversed ListView showing messages from my Firestore messages subcollection. Messages sent by me should be blue and right-aligned. Messages from others should be grey and left-aligned. Add a text input and send button pinned at the bottom. Connect everything with real-time queries.
Frequently asked questions
How do I create a new conversation between two users?
Before creating a new conversation, query conversations where participants arrayContains both user UIDs to check if one already exists. If not, create a new conversation document with participants: [currentUser.uid, otherUser.uid], lastMessage: empty, lastMessageTime: now, and unreadCount: {uid1: 0, uid2: 0}. Then navigate to the ChatPage.
How do I implement group chat with more than two participants?
The same data model supports group chat. Add 3+ UIDs to the participants array. On the conversations list, show a group name instead of a single user's name. In the chat, display the sender's name above each message bubble. The unreadCount map scales to any number of participants.
Why do my conversations show as empty even though data exists in Firestore?
This is almost always a missing composite index. Firestore requires a composite index for arrayContains on participants combined with orderBy on lastMessageTime. Check the Firebase Console logs for the auto-generated index creation URL, or create it manually.
How do I show typing indicators?
Add a typingUsers map to the conversation document (e.g., {uid_alice: true}). When a user types in the TextField, update the map to set their UID to true. After 3 seconds of inactivity (use a debounce Timer), set it back to false. On the other user's chat screen, show 'typing...' when the map has the other user's UID set to true.
Can I add read receipts to show when messages are seen?
Add a readBy List field to each message document. When a user opens the chat, update all unread messages to add their UID to the readBy list. Display a double-check icon on sent messages when readBy contains the recipient's UID. This works for both one-on-one and group chats.
Can RapidDev help build a production chat system with push notifications and moderation?
Yes. A production chat app needs push notifications via FCM when the app is backgrounded, message moderation and reporting, media compression, message search, and message retention policies. RapidDev can build the full system with Cloud Functions and FCM integration.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation