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

- Tool: FlutterFlow
- Difficulty: Intermediate
- Time required: 1.5-3 hours
- Compatibility: FlutterFlow Free+ with Firebase Firestore
- Last updated: March 2026

## 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.

## Before you start

- 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 code example

File: `chat_firestore_schema.txt`

```text
Firestore Chat Data Model

conversations/{conversationId}
  participants: String[]  // [uid1, uid2]
  participantNames: Map  // {uid1: 'Alice', uid2: 'Bob'}
  lastMessage: String
  lastMessageTime: Timestamp
  lastMessageSenderId: String
  typingUserId: String?  // for typing indicator
  createdAt: Timestamp

conversations/{conversationId}/messages/{messageId}
  senderId: String
  senderName: String  // denormalized for fast display
  text: String
  mediaUrl: String?  // for image messages
  mediaType: 'text' | 'image' | 'file'
  timestamp: Timestamp
  readBy: String[]  // UIDs who have read this message

QUERIES:
  Conversation list:
    WHERE participants arrayContains currentUid
    ORDER BY lastMessageTime DESC
    Single Time Query: OFF

  Messages:
    COLLECTION: conversations/{id}/messages
    ORDER BY timestamp DESC
    LIMIT 50
    Single Time Query: OFF

FLUTTERFLOW KEY SETTINGS:
  ListView: Reverse Scroll = true
  (newest message anchors to bottom)

SEND MESSAGE ACTION FLOW:
  1. Add Document → messages subcollection
  2. Update Document → conversation.lastMessage
  3. Clear messageText Page State
  4. Scroll ListView to bottom
```

## Common mistakes

- **Storing messages as an Array inside the conversation document instead of a subcollection** — Firestore documents have a 1MB size limit. A chat conversation with 1,000 messages (average 200 bytes each) is 200KB — well under the limit. But with longer messages, media URLs, and reaction data, documents hit 1MB quickly. Also, arrays cannot be paginated — you always load all messages, which is slow and expensive for long conversations. Fix: 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.
- **Using Single Time Query: ON (default) for the messages Backend Query** — Single Time Query fetches messages once when the page loads and never updates. New messages sent by the other user do not appear until the current user navigates away and back. This makes the chat feel broken — users think messages are not being sent. Fix: 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.
- **Ordering messages ascending and not reversing the ListView** — If messages are ordered ascending (oldest first) and the ListView is not reversed, new messages appear at the bottom and the user must scroll down to see them. Standard chat behavior is newest at the bottom with auto-scroll — which requires ordering descending (newest first) and reversing the ListView so index 0 (newest) anchors to the screen bottom. Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-integrate-a-chat-system-with-database-driven-messages-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-integrate-a-chat-system-with-database-driven-messages-in-flutterflow
