Thursday, November 27, 2025

thumbnail

Building a Scalable Chat App with Firestore and Firebase Authentication

 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

Get Directions 

Subscribe by Email

Follow Updates Articles from This Blog via Email

No Comments

About

Search This Blog

Powered by Blogger.

Blog Archive