Real-time chat is one of the most instructive projects you can build โ€” it touches authentication, live data sync, file storage, and push notifications all at once. Here's the Firebase architecture I used for MeeChat, my production messenger app.

Firebase Stack Overview

A complete chat app needs four Firebase products working together:

  • Firebase Auth โ€” Google Sign-In or email/password, with UID-based permissions
  • Cloud Firestore โ€” message storage, ordered queries, real-time listeners
  • Firebase Realtime Database โ€” online presence (typing indicators, last seen)
  • Firebase Cloud Messaging (FCM) โ€” push notifications when app is backgrounded
  • Firebase Storage โ€” image/file uploads for media messages

Firestore Data Model

Design your collections around your read patterns. For a 1:1 chat app, a flat conversations collection with sub-collection messages works well and avoids expensive joins.

text
Firestore structure:
/users/{uid}
  displayName, photoUrl, fcmToken, lastSeen

/conversations/{convoId}
  participants: [uid1, uid2]
  lastMessage: { text, senderId, timestamp }
  updatedAt: Timestamp

/conversations/{convoId}/messages/{msgId}
  text, senderId, timestamp, type (text|image)
  readBy: [uid1, uid2]

Real-time Message Stream

Firestore's snapshots() stream is the core of the chat experience. Wrap it in a StreamBuilder for automatic UI updates.

dart
Stream<List<Message>> getMessages(String convoId) {
  return FirebaseFirestore.instance
      .collection('conversations/\$convoId/messages')
      .orderBy('timestamp', descending: true)
      .limit(50)
      .snapshots()
      .map((snap) => snap.docs
          .map((doc) => Message.fromDoc(doc))
          .toList());
}

Online Presence with Realtime Database

Firestore isn't ideal for ephemeral state like online presence โ€” it has per-document write limits. Use Firebase Realtime Database with onDisconnect() for reliable presence:

dart
final presenceRef = FirebaseDatabase.instance
    .ref('presence/\$uid');

// Set online when app opens
await presenceRef.set({'online': true, 'lastSeen': ServerValue.TIMESTAMP});

// Auto-set offline on disconnect (handled server-side)
await presenceRef.onDisconnect().set({
  'online': false,
  'lastSeen': ServerValue.TIMESTAMP,
});

Security Rules

Never skip Firestore security rules. Lock messages to conversation participants:

javascript
rules_version = '2';
service cloud.firestore {
  match /databases/{db}/documents {
    match /conversations/{convoId}/messages/{msgId} {
      allow read, write: if request.auth.uid in
        get(/databases/$(db)/documents/conversations/$(convoId)).data.participants;
    }
  }
}

Key Lessons from Production

After running MeeChat with real users, the most important learnings were:

  • Paginate messages with startAfterDocument โ€” never load entire conversation history
  • Store FCM tokens on login and update on token refresh to avoid stale notifications
  • Use Firestore transactions for read receipts to avoid race conditions
  • Add retry logic for image uploads โ€” Storage sends can fail on poor connections

Written by Iqbal Nova

Mobile Developer @ GMEDIA ยท Mobile Developer specializing in Flutter & React Native.