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