High-level architecture
Data model for 1:1 and group chats
Firebase Auth setup
Firestore read/write patterns for scalability
Security rules (very important!)
Real-time listeners & pagination
Typing indicators / presence (online/offline)
Cost & performance tips
1. High-Level Architecture
Client (Web / Android / iOS / Flutter, etc.)
⬇
Firebase Authentication – handles login (email/password, Google, etc.)
⬇
Cloud Firestore – stores users, conversations, messages
⬇
(Optional) Cloud Functions – for triggers: notifications, fan-out, cleanup, etc.
Everything is essentially “serverless”: Auth + Firestore + Functions.
2. Data Model
A scalable chat app is 99% about your data model. Design it so reads are cheap and predictable.
2.1 Collections Overview
You’ll typically have:
users – profile info, last seen, avatar, etc.
conversations – 1:1 or group chat metadata
messages – subcollection under each conversation
2.2 Example Structure
/users/{userId}
displayName: string
photoURL: string
email: string
createdAt: timestamp
lastSeen: timestamp
/conversations/{conversationId}
isGroup: boolean
createdAt: timestamp
lastMessage: string
lastMessageAt: timestamp
members: [userId, ...] // array of user IDs
memberRoles: { userId: "admin" | "member" }
/conversations/{conversationId}/messages/{messageId}
senderId: userId
text: string
createdAt: timestamp
type: "text" | "image" | ...
readBy: [userId, ...] // for read receipts (optional)
2.3 1:1 vs Group Chats
1:1 chat: members has exactly 2 user IDs.
Group chat: members can be many; use memberRoles for admin, etc.
You can also create an index like:
/userConversations/{userId}/items/{conversationId}
conversationId: string
lastMessageAt: timestamp
pinned: boolean
This speeds up the “chat list” view per user, avoids heavy queries on conversations with complex filters.
3. Firebase Authentication Setup
3.1 Enable Providers
In the Firebase Console:
Go to Build → Authentication → Sign-in method
Enable providers: Email/Password, Google, etc.
3.2 Client Code (example: Web / JS)
import { initializeApp } from "firebase/app";
import { getAuth, signInWithPopup, GoogleAuthProvider } from "firebase/auth";
const firebaseConfig = { /* your config */ };
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const provider = new GoogleAuthProvider();
async function signInWithGoogle() {
const result = await signInWithPopup(auth, provider);
const user = result.user;
// Create user document in Firestore if not exists
}
3.3 Create/Sync User Document
On first login, create /users/{uid}:
import { getFirestore, doc, setDoc, serverTimestamp } from "firebase/firestore";
const db = getFirestore(app);
async function createUserIfNeeded(user) {
const ref = doc(db, "users", user.uid);
await setDoc(ref, {
displayName: user.displayName || "",
photoURL: user.photoURL || "",
email: user.email || "",
createdAt: serverTimestamp(),
lastSeen: serverTimestamp()
}, { merge: true });
}
You can also automate this with a Cloud Function on user creation, but client-side is fine to start.
4. Writing Messages & Conversation Updates
4.1 Sending a New Message
Use a batched write or transaction so the message and conversation metadata stay consistent.
import { collection, doc, writeBatch, serverTimestamp } from "firebase/firestore";
async function sendMessage(conversationId, userId, text) {
const batch = writeBatch(db);
const messagesRef = collection(db, "conversations", conversationId, "messages");
const newMessageRef = doc(messagesRef); // auto ID
batch.set(newMessageRef, {
senderId: userId,
text,
createdAt: serverTimestamp(),
type: "text",
readBy: [userId]
});
const convRef = doc(db, "conversations", conversationId);
batch.update(convRef, {
lastMessage: text,
lastMessageAt: serverTimestamp()
});
await batch.commit();
}
4.2 Creating a Conversation
import { addDoc, collection, serverTimestamp } from "firebase/firestore";
async function createConversation(memberIds, isGroup = false) {
const convRef = await addDoc(collection(db, "conversations"), {
isGroup,
members: memberIds,
createdAt: serverTimestamp(),
lastMessage: "",
lastMessageAt: serverTimestamp(),
});
// Optionally create /userConversations for each member
// for fast listing.
return convRef.id;
}
5. Firestore Security Rules (Core Ideas)
You want to ensure:
Only authenticated users can access data.
Users can only read/write conversations they are members of.
Users can only modify their own profile.
5.1 Basic Auth Check
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isSignedIn() {
return request.auth != null;
}
function isConversationMember(conversation) {
return isSignedIn() && (request.auth.uid in conversation.members);
}
5.2 Users Collection Rules
match /users/{userId} {
allow read: if isSignedIn();
allow write: if isSignedIn() && request.auth.uid == userId;
}
5.3 Conversations & Messages Rules
match /conversations/{conversationId} {
allow read, update: if isConversationMember(resource.data);
allow create: if isSignedIn();
match /messages/{messageId} {
allow read, create: if isConversationMember(
get(/databases/$(database)/documents/conversations/$(conversationId)).data
);
// Restrict updates/deletes if needed
}
}
}
}
These are just starting rules; you’ll refine them as you add features (attachments, roles, etc.).
6. Real-Time Listeners & Pagination
6.1 Real-Time Message Stream
To show messages live (like WhatsApp/Telegram):
import { query, collection, orderBy, limit, onSnapshot } from "firebase/firestore";
function subscribeToMessages(conversationId, onMessages) {
const messagesRef = collection(db, "conversations", conversationId, "messages");
const q = query(messagesRef, orderBy("createdAt", "desc"), limit(30));
return onSnapshot(q, (snapshot) => {
const messages = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
// Reverse if you want ascending order in UI
onMessages(messages.reverse());
});
}
6.2 Pagination (Load More)
Use startAfter with the oldest loaded message:
import { startAfter } from "firebase/firestore";
async function loadMoreMessages(conversationId, lastVisibleDoc) {
const messagesRef = collection(db, "conversations", conversationId, "messages");
const q = query(
messagesRef,
orderBy("createdAt", "desc"),
startAfter(lastVisibleDoc),
limit(30)
);
const snapshot = await getDocs(q);
// Append results to existing messages in UI
}
6.3 Chat List (Conversations for a User)
Use userConversations for fast loading:
const q = query(
collection(db, "userConversations", userId, "items"),
orderBy("lastMessageAt", "desc"),
limit(20)
);
onSnapshot(q, ...)
Or query conversations with where('members', 'array-contains', userId) for simpler but potentially heavier reads.
7. Presence & Typing Indicators
Firestore alone isn’t ideal for ephemeral states (typing, online status), but you can still do it.
7.1 Online / Last Seen
Simpler approach: each client updates users/{uid}.lastSeen periodically and on disconnect (if using Realtime Database or Functions).
If you only have Firestore on client, you can:
Update lastSeen when app becomes active or a message is sent.
Treat “online” if lastSeen within X seconds/minutes.
7.2 Typing Indicator
Use a small doc per conversation:
/conversations/{conversationId}/typing/{userId}
isTyping: boolean
updatedAt: timestamp
Client:
Set isTyping = true when user starts typing; turn it off with debounce (e.g., after 1–2 seconds of inactivity).
Subscribe to typing docs for other users to show “User is typing…”
This is extra read/write cost; consider optimizing (e.g., only for active chats).
8. Scalability, Performance & Cost Tips
8.1 Minimize Hotspots
Avoid updating the same doc too frequently from many clients (e.g., don’t let all users constantly write to a single global doc).
For high-traffic metrics (message counts, unread counts), use Cloud Functions to aggregate or periodic updates instead of every event.
8.2 Keep Documents Small
Firestone doc limit ≈ 1 MiB. For chat you’re usually fine, but:
Don’t store huge arrays (like thousands of members or readBy entries) on a single document.
Instead, use additional collections (e.g., /conversationMembers, /messageReads) if needed.
8.3 Indexes
Create composite indexes for frequent queries (e.g., members + lastMessageAt).
Check the Firebase console when you see “index not defined” errors.
8.4 Limit Listeners
Only attach real-time listeners to what’s visible:
Currently open conversation’s messages.
Top N conversations in the list.
Unsubscribe listeners when the screen is not visible to avoid unnecessary reads.
8.5 Use serverTimestamp()
Always rely on serverTimestamp() to avoid time skews between clients.
9. Optional: Cloud Functions Goodies
You can enhance your app with Cloud Functions:
Push notifications when a new message is written:
Trigger on conversations/{id}/messages/{id} create.
Fan-out updates to userConversations for each member when a message is sent.
Moderation: simple content checks, profanity filters, etc.
Example (TypeScript-ish pseudo-code):
exports.onMessageCreate = functions.firestore
.document("conversations/{conversationId}/messages/{messageId}")
.onCreate(async (snap, context) => {
const message = snap.data();
const conversationId = context.params.conversationId;
// Update userConversations, send notifications, etc.
});
10. Putting It All Together
Set up Firebase project and enable Auth + Firestore.
Implement Auth (e.g., Google sign-in) and create /users/{uid} docs.
Define data model: users, conversations, messages, optional userConversations.
Implement send message + real-time listeners for messages and chat list.
Write Firestore rules to protect user data and enforce membership.
Add extras: typing, read receipts, last seen, push notifications.
Monitor Firestore usage & cost, refine queries and indexes.
Learn GCP Training in Hyderabad
Read More
Cloud SQL, Firestore, Bigtable - Advanced Database Workflows
Handling Data Skew in Large Dataproc Jobs
Spark SQL Tuning Techniques on Dataproc
Using Dataproc with JupyterHub for Collaborative Data Science
Visit Our Quality Thought Training Institute in Hyderabad
Subscribe by Email
Follow Updates Articles from This Blog via Email
No Comments