Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Build a Chat System with Database-Driven Messages in FlutterFlow

Build a database-driven chat in FlutterFlow using Firestore's subcollection structure: conversations/{id}/messages/{msgId}. Enable real-time by turning off Single Time Query on the messages Backend Query. Messages ordered by timestamp descending with a reversed ListView. Add read receipts by updating lastReadAt on the conversation document when the user opens the chat. Add unread count by comparing lastReadAt with conversation.lastMessageTime.

What you'll learn

  • How to design the Firestore data model for chat conversations and messages using subcollections
  • How to configure real-time message listeners in FlutterFlow Backend Queries
  • How to implement read receipts and unread message counts using Firestore timestamps
  • How to add a typing indicator that shows when the other user is composing a message
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate9 min read1.5-3 hoursFlutterFlow Free+ with Firebase FirestoreMarch 2026RapidDev Engineering Team
TL;DR

Build a database-driven chat in FlutterFlow using Firestore's subcollection structure: conversations/{id}/messages/{msgId}. Enable real-time by turning off Single Time Query on the messages Backend Query. Messages ordered by timestamp descending with a reversed ListView. Add read receipts by updating lastReadAt on the conversation document when the user opens the chat. Add unread count by comparing lastReadAt with conversation.lastMessageTime.

Chat data model: why subcollections beat arrays for messages

The wrong Firestore structure makes chat slow and unreliable. Storing messages as an array inside a conversation document hits Firestore's 1MB document limit after a few hundred messages and prevents efficient pagination. The correct structure uses subcollections: conversations/{conversationId}/messages/{messageId}. Each message is an independent document with its own ID, timestamp, sender, and content. You query and paginate the messages subcollection separately from the conversation metadata. This tutorial focuses on the database integration mechanics — the data model, real-time listening, message ordering, pagination, read receipts, and typing indicators — that make a chat feel responsive and reliable at any scale.

Prerequisites

  • FlutterFlow project with Firebase Firestore and Authentication enabled
  • Basic familiarity with Backend Queries and Action Flows in FlutterFlow
  • Firebase Blaze plan for production apps (Firestore reads/writes scale beyond Spark free tier limits quickly in chat)

Step-by-step guide

1

Design the Firestore chat data model

Create two collections in FlutterFlow's Firestore panel: conversations and a messages subcollection. The conversations collection documents contain: conversationId (auto-generated), participants (Array of String UIDs), participantNames (Map: {uid: displayName} for fast display), lastMessage (String: preview of last message), lastMessageTime (Timestamp), lastMessageSenderId (String UID), unreadCount (Map: {uid: Integer} — per-participant unread counts), createdAt (Timestamp). The messages subcollection under each conversation has: messageId (auto-generated), senderId (String UID), senderName (String: copied from user profile at send time), text (String), mediaUrl (String, nullable), mediaType (String: 'text', 'image', 'file'), timestamp (Timestamp), readBy (Array of String UIDs). This flat, denormalized design avoids expensive JOINs and makes reads fast.

Expected result: Conversations and messages Firestore collections are configured with the correct fields and types.

2

Build the conversations list page with unread counts

Create a Conversations page. Add a Backend Query on the page fetching the conversations collection filtered by: participants arrayContains Current User UID, ordered by lastMessageTime descending. Single Time Query: OFF (real-time updates when new messages arrive). Add a ListView bound to the query results. Each conversation list item shows: an Avatar (CircleImage), the participant name (from participantNames map, showing the other participant's name), a preview of lastMessage, a formatted relative time from lastMessageTime (use a Custom Function to format Timestamp as 'just now', '5m ago', '2h ago'), and an unread badge (Container with red background and bold text showing the unread count from unreadCount[currentUserUid]). Conditionally show the unread badge only when unreadCount[currentUserUid] > 0.

Expected result: The conversations list shows all user conversations in reverse chronological order with live unread message counts.

3

Set up the real-time messages view with correct ordering

Create a ChatDetail page with a Page Parameter named conversationId (String). Add a Backend Query on the page for the messages subcollection at path conversations/{conversationId}/messages, ordered by timestamp DESCENDING, limit 50. Single Time Query: OFF for real-time. The descending order puts the newest message at the top of the query result, but you want it at the bottom of the screen. Add a ListView to the page. In the ListView settings, check the Reverse Scroll option — this flips the list so index 0 (newest message) appears at the bottom and older messages scroll upward. Each message item is a Row: the sender's avatar (small, 32px), a message bubble Container (your message: right-aligned, blue background; other's message: left-aligned, grey background — use Conditional Widget to swap alignment based on senderId == CurrentUser.uid).

Expected result: The chat view shows messages in correct order with newest at the bottom, updating in real-time as new messages arrive.

4

Send messages and update the conversation's lastMessage metadata

Add a message input row at the bottom of the ChatDetail page: a TextField (Page State variable: messageText) and a Send IconButton. The Send button's action flow: (1) Add Document to conversations/{conversationId}/messages with {senderId: currentUser.uid, senderName: currentUser.displayName, text: messageText, timestamp: FieldValue.serverTimestamp(), mediaType: 'text', readBy: [currentUser.uid]}. (2) Update Document on conversations/{conversationId} with {lastMessage: messageText, lastMessageTime: FieldValue.serverTimestamp(), lastMessageSenderId: currentUser.uid}. (3) Clear the messageText Page State variable. Use FieldValue.serverTimestamp() via a Custom Function or set timestamp to Current Date Time for the timestamp field — server timestamp is preferred for consistency across time zones.

Expected result: Messages appear instantly in the chat view and the conversation list preview updates with the latest message.

5

Implement read receipts and typing indicator

Read receipts: when the user opens a ChatDetail page, in the On Page Load action, call Update Document on conversations/{conversationId} setting lastReadAt_{currentUserUid}: serverTimestamp(). To show read status on your sent messages, check if the other participant's UID is in the message's readBy array. Typing indicator: add a typingUserId (String, nullable) field to the conversation document. When the user starts typing in the TextField (On TextField Change), call Update Document setting typingUserId to the current user's UID. Use a debounce Custom Action — after 2 seconds of no typing, set typingUserId to null. In the chat UI, show a 'User is typing...' text Container above the input row when typingUserId is not null AND typingUserId != currentUser.uid. The typing indicator updates in real-time via the conversation document's real-time listener.

Expected result: The chat shows read receipts on sent messages and a live typing indicator when the other user is composing a message.

Complete working example

chat_firestore_schema.txt
1Firestore Chat Data Model
2
3conversations/{conversationId}
4 participants: String[] // [uid1, uid2]
5 participantNames: Map // {uid1: 'Alice', uid2: 'Bob'}
6 lastMessage: String
7 lastMessageTime: Timestamp
8 lastMessageSenderId: String
9 typingUserId: String? // for typing indicator
10 createdAt: Timestamp
11
12conversations/{conversationId}/messages/{messageId}
13 senderId: String
14 senderName: String // denormalized for fast display
15 text: String
16 mediaUrl: String? // for image messages
17 mediaType: 'text' | 'image' | 'file'
18 timestamp: Timestamp
19 readBy: String[] // UIDs who have read this message
20
21QUERIES:
22 Conversation list:
23 WHERE participants arrayContains currentUid
24 ORDER BY lastMessageTime DESC
25 Single Time Query: OFF
26
27 Messages:
28 COLLECTION: conversations/{id}/messages
29 ORDER BY timestamp DESC
30 LIMIT 50
31 Single Time Query: OFF
32
33FLUTTERFLOW KEY SETTINGS:
34 ListView: Reverse Scroll = true
35 (newest message anchors to bottom)
36
37SEND MESSAGE ACTION FLOW:
38 1. Add Document messages subcollection
39 2. Update Document conversation.lastMessage
40 3. Clear messageText Page State
41 4. Scroll ListView to bottom

Common mistakes

Why it's a problem: Storing messages as an Array inside the conversation document instead of a subcollection

How to avoid: Use the subcollection structure: conversations/{id}/messages/{msgId}. Each message is its own document. You can paginate with limit and startAfterDocument, query specific date ranges, and the conversation document stays small regardless of message count.

Why it's a problem: Using Single Time Query: ON (default) for the messages Backend Query

How to avoid: Set Single Time Query to OFF on the messages Backend Query. This creates a real-time Firestore listener that pushes new messages to the app within 1-2 seconds of them being written.

Why it's a problem: Ordering messages ascending and not reversing the ListView

How to avoid: Order the messages Backend Query by timestamp DESCENDING. In the ListView settings, enable the Reverse Scroll option. This combination makes new messages appear at the bottom of the screen automatically.

Best practices

  • Denormalize sender name and avatar into each message document — this avoids a JOIN to the users collection for every message displayed in the ListView
  • Implement infinite scroll pagination for older messages — load the initial 50 messages and load more when the user scrolls to the top of the chat history
  • Use Firestore's FieldValue.serverTimestamp() for message timestamps, not the device's local time — prevents time ordering issues when devices have incorrect clocks
  • Cap typing indicator updates at one Firestore write per 2 seconds using debounce logic — typing indicators without debounce burn Firestore write quota
  • Store the other participant's push token in the conversation document and trigger a Cloud Function on new message writes to send a push notification
  • Add a message delivery status by updating a status field (sent, delivered, read) — update to delivered when the recipient opens the conversation
  • Use Firestore security rules to enforce that senderId in the message must match the writing user's UID to prevent message spoofing

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I am building a FlutterFlow chat app using Firestore. Write me the Firestore security rules for a chat system with this data model: conversations collection with participants array field, and messages subcollection under each conversation. Rules should: (1) allow users to read conversations where their UID is in the participants array, (2) allow users to read messages only in conversations they are participants of, (3) allow users to create messages only in their own conversations with senderId matching their UID, (4) prevent users from editing or deleting messages.

FlutterFlow Prompt

Build the chat message list page in FlutterFlow. The page should have: a Backend Query on the messages subcollection ordered by timestamp descending with Single Time Query OFF, a reversed ListView bound to the query results, message bubbles aligned right for messages from the current user and left for others (with different background colors), and a bottom input row with a TextField and Send button. When Send is tapped, add the message document to the subcollection and update the parent conversation's lastMessage field.

Frequently asked questions

How do I load older messages as the user scrolls up?

Implement cursor-based pagination for the messages subcollection. Store the oldest loaded message's DocumentSnapshot in a Page State variable named oldestMessageSnapshot. Add an InfiniteScroll or a Load Earlier Messages button. When triggered, run the messages Backend Query again with the same order and limit but add a startAfterDocument parameter bound to oldestMessageSnapshot. Prepend the new results to your existing messages list in App State and update oldestMessageSnapshot to the oldest document in the new batch.

How do I send image messages in the chat?

Add a media attachment button (image picker icon) next to the text input. On tap, open the device image picker via a Custom Action. Upload the selected image to Firebase Storage at chatMedia/{conversationId}/{timestamp}_{filename}. After successful upload, get the download URL. Then send a message document with mediaUrl set to the download URL, mediaType set to 'image', and text set to an empty string or a placeholder like 'Photo'. In the chat ListView, render image messages with an Image widget (showing the mediaUrl) instead of a Text widget.

Can I build a group chat with this same data model?

Yes, with minor modifications. The participants array supports any number of UIDs — just add all group member UIDs. participantNames becomes a map with all members. For group unread counts, maintain a lastReadAt map keyed by each participant's UID and compute per-user unread counts by counting messages with timestamps newer than lastReadAt[uid]. The messages subcollection pattern handles group chats identically to 1-to-1 chats.

How do I send push notifications for new messages?

Store each user's FCM (Firebase Cloud Messaging) push token in their user profile document. Create a Firestore trigger Cloud Function that fires on new document creation in conversations/{conversationId}/messages/{messageId}. In the function, read the other participants' FCM tokens and send a push notification via admin.messaging().sendMulticast(). Include the message text, sender name, and conversationId in the notification payload so the app navigates directly to the correct conversation when the notification is tapped.

How do I delete messages and handle message reactions?

Message deletion: add a long-press action on message bubbles that shows an action sheet (Delete). The Delete action updates the message document setting text to 'This message was deleted' and deleted: true, rather than actually deleting the document — this preserves message threading for replies and prevents gaps in the conversation history. Message reactions: add a reactions field as a Map on each message document ({emoji: [uid1, uid2]}). Tapping a reaction emoji updates the reactions field using FieldValue.arrayUnion and FieldValue.arrayRemove for toggle behavior.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.