Skip to main content

Firebase Data Model

This document outlines the data structure used in the Firebase Realtime Database for the chat application, explaining how data is organized and relationships are maintained.

Database Structure Overview

The chat application uses Firebase Realtime Database to store and synchronize data in real-time. The data is organized hierarchically in a JSON-like structure.

Database Structure Diagram

Main Data Collections

The database is structured with these primary collections:

firebase-database/
├── users/ # User profiles and metadata
├── chats/ # Chat conversation metadata
├── messages/ # Chat messages
├── userChats/ # User-chat relationships
├── typing/ # Typing indicators
└── presence/ # User online status

Users Collection

The users collection stores user profiles and related information:

"users": {
"userId1": {
"displayName": "John Doe",
"email": "john@example.com",
"photoURL": "https://example.com/photos/john.jpg",
"createdAt": 1642527812354,
"lastActive": 1642789436781,
"status": "online",
"blockedUsers": {
"userId3": true,
"userId7": true
},
"preferences": {
"notifications": {
"browserNotifications": true,
"soundEnabled": true,
"mentionAlerts": true
},
"theme": "dark"
}
},
"userId2": {
// User 2 data
}
}

User Object Properties

PropertyTypeDescription
displayNamestringUser's display name
emailstringUser's email address
photoURLstringURL to user's profile picture
createdAtnumberTimestamp when user was created
lastActivenumberTimestamp of last user activity
statusstringCurrent user status: "online", "offline", "away"
blockedUsersobjectMap of blocked user IDs to boolean
preferencesobjectUser preferences for app settings

Chats Collection

The chats collection stores metadata about chat conversations:

"chats": {
"chatId1": {
"type": "private",
"createdAt": 1642527892354,
"lastMessageTime": 1642789436781,
"participants": {
"userId1": true,
"userId2": true
},
"lastMessage": {
"content": "Hello there!",
"sender": "userId1",
"timestamp": 1642789436781,
"type": null
}
},
"chatId2": {
"type": "group",
"name": "Project Team",
"createdBy": "userId1",
"createdAt": 1642525692354,
"lastMessageTime": 1642788436781,
"photoURL": "https://example.com/groups/project-team.jpg",
"description": "Team chat for the new project",
"members": {
"userId1": {
"role": "admin",
"joinedAt": 1642525692354
},
"userId2": {
"role": "member",
"joinedAt": 1642525812354
},
"userId3": {
"role": "member",
"joinedAt": 1642526912354
}
},
"lastMessage": {
"content": "Meeting tomorrow at 10",
"sender": "userId3",
"timestamp": 1642788436781,
"type": null
}
}
}

Chat Object Properties

PropertyTypeDescription
typestringType of chat: "private" or "group"
createdAtnumberTimestamp when chat was created
lastMessageTimenumberTimestamp of last message
participantsobjectFor private chats: Map of participant user IDs
membersobjectFor group chats: Map of member user IDs with roles
namestringFor group chats: Display name of the group
photoURLstringFor group chats: Group profile picture URL
createdBystringFor group chats: User ID of creator
descriptionstringFor group chats: Group description
lastMessageobjectPreview of the last message sent

Messages Collection

The messages collection stores all chat messages, organized by chat ID:

"messages": {
"chatId1": {
"messageId1": {
"content": "Hello there!",
"sender": "userId1",
"timestamp": 1642789436781,
"readBy": {
"userId1": 1642789436781,
"userId2": 1642789496781
},
"replyTo": null,
"edited": false,
"reactions": {
"👍": {
"userId2": true
}
}
},
"messageId2": {
"content": "Hi! How are you?",
"sender": "userId2",
"timestamp": 1642789536781,
"readBy": {
"userId1": 1642789596781,
"userId2": 1642789536781
},
"replyTo": "messageId1",
"edited": false
},
"messageId3": {
"type": "file",
"fileName": "document.pdf",
"fileSize": 2457862,
"fileURL": "https://storage.example.com/files/document.pdf",
"fileThumbnail": "https://storage.example.com/thumbnails/document.jpg",
"sender": "userId1",
"timestamp": 1642789636781,
"readBy": {
"userId1": 1642789636781
}
},
"messageId4": {
"type": "voice",
"duration": 12.5,
"fileURL": "https://storage.example.com/voice/recording.mp3",
"sender": "userId2",
"timestamp": 1642789736781,
"readBy": {
"userId2": 1642789736781
}
}
},
"chatId2": {
// Messages for chat 2
}
}

Message Object Properties

PropertyTypeDescription
contentstringMessage text content (for text messages)
senderstringUser ID of message sender
timestampnumberTimestamp when message was sent
readByobjectMap of user IDs to timestamp when they read the message
replyTostringID of message being replied to (null if not a reply)
editedbooleanWhether message has been edited
reactionsobjectMap of emoji reactions to user IDs
typestringMessage type: null (text), "file", "voice", etc.
fileNamestringFor file messages: Original filename
fileSizenumberFor file messages: File size in bytes
fileURLstringFor file/voice messages: URL to stored file
fileThumbnailstringFor image file messages: URL to thumbnail
durationnumberFor voice messages: Duration in seconds
deletedbooleanWhether message has been deleted

UserChats Collection

The userChats collection maintains a record of which chats a user belongs to:

"userChats": {
"userId1": {
"chatId1": {
"unreadCount": 0,
"lastReadTime": 1642789596781,
"pinned": true
},
"chatId2": {
"unreadCount": 5,
"lastReadTime": 1642785436781,
"pinned": false
}
},
"userId2": {
// Chats for user 2
}
}

This structure provides a quick way to:

  • Retrieve all chats for a specific user
  • Track unread message counts
  • Maintain user-specific chat settings like pinned status

UserChat Object Properties

PropertyTypeDescription
unreadCountnumberNumber of unread messages
lastReadTimenumberTimestamp when user last read the chat
pinnedbooleanWhether the chat is pinned by the user
mutedbooleanWhether notifications are muted for this chat

Typing Indicators

The typing collection stores real-time typing indicators:

"typing": {
"chatId1": {
"userId1": {
"isTyping": false,
"timestamp": 1642789836781
},
"userId2": {
"isTyping": true,
"timestamp": 1642789896781
}
}
}

Typing Object Properties

PropertyTypeDescription
isTypingbooleanWhether user is currently typing
timestampnumberTimestamp of last typing status update

Presence System

The presence collection maintains user online status:

"presence": {
"userId1": {
"online": true,
"lastChanged": 1642789936781
},
"userId2": {
"online": false,
"lastChanged": 1642788936781
}
}

Firebase's built-in presence system is also utilized for more reliable online status tracking.

Data Relationships

User and Chat Relationships

  • Direct Messages: Two users share a private chat
  • Group Chats: Multiple users share a group chat with roles
  • Blocking: One-way relationship where a user blocks another

Message Relationships

  • Reply Chains: Messages can reference other messages
  • Reactions: Users can react to messages
  • Read Status: Tracking which users have read which messages

Security Rules

Firebase security rules control access to the data based on user authentication and relationships. Here's a simplified example of the rules:

{
"rules": {
"users": {
"$userId": {
// Users can read all user profiles
".read": "auth !== null",
// Users can write only to their own profile
".write": "auth !== null && auth.uid === $userId"
}
},
"chats": {
"$chatId": {
// Users can read chats they participate in
".read": "auth !== null && root.child('chats').child($chatId).child('participants').child(auth.uid).exists() ||
root.child('chats').child($chatId).child('members').child(auth.uid).exists()",
// Similar write rules with additional checks for group admin actions
}
},
"messages": {
"$chatId": {
// Users can read messages from chats they participate in
".read": "auth !== null && root.child('chats').child($chatId).child('participants').child(auth.uid).exists() ||
root.child('chats').child($chatId).child('members').child(auth.uid).exists()",
"$messageId": {
// Users can create messages in chats they participate in
".write": "auth !== null &&
(root.child('chats').child($chatId).child('participants').child(auth.uid).exists() ||
root.child('chats').child($chatId).child('members').child(auth.uid).exists()) &&
// Prevent writing if blocked
!root.child('users').child(data.child('sender').val()).child('blockedUsers').child(auth.uid).exists()"
}
}
}
}
}

Data Consistency and Integrity

To maintain data consistency and integrity, the application follows these practices:

  1. Transactions: Using Firebase transactions for operations that need atomic updates
  2. Denormalization: Storing redundant data in multiple places for performance
  3. Validation: Validating data on both client and server (via security rules)
  4. Offline Support: Handling offline/online synchronization gracefully
  5. Cleanup Functions: Using Firebase Cloud Functions for cleanup tasks and maintaining consistency

Optimization Techniques

Several optimization techniques are used to improve performance:

  1. Pagination: Loading messages in batches using startAt, endAt, and limitToLast
  2. Shallow Queries: Using .shallow() to retrieve only keys when full objects aren't needed
  3. Indexing: Creating proper Firebase indexes for common queries
  4. Data Segmentation: Breaking large data sets into manageable chunks
  5. Caching: Leveraging Firebase's built-in caching mechanisms

Example Queries

Here are some common query patterns used in the application:

Get a User's Chats

// Get all chats for a user
const userChatsRef = ref(db, `userChats/${userId}`);
onValue(userChatsRef, (snapshot) => {
const userChats = snapshot.val() || {};

// Process user chats
const chatsArray = Object.entries(userChats).map(([chatId, chatData]) => ({
id: chatId,
...chatData
}));
});

Get Chat Messages with Pagination

// Get last 20 messages for a chat
const messagesRef = ref(db, `messages/${chatId}`);
const messagesQuery = query(messagesRef, orderByChild('timestamp'), limitToLast(20));

onValue(messagesQuery, (snapshot) => {
const messages = [];
snapshot.forEach((childSnapshot) => {
messages.push({
id: childSnapshot.key,
...childSnapshot.val()
});
});

// Sort messages by timestamp
messages.sort((a, b) => a.timestamp - b.timestamp);
});

Check if User Is Blocked

// Check if user2 has blocked user1
const blockedRef = ref(db, `users/${user2Id}/blockedUsers/${user1Id}`);
get(blockedRef).then((snapshot) => {
const isBlocked = snapshot.exists() && snapshot.val() === true;

if (isBlocked) {
console.log("User is blocked");
} else {
console.log("User is not blocked");
}
});

Best Practices

When working with the Firebase data model, follow these best practices:

  1. Flatten Data: Avoid deeply nested data structures
  2. Listen Efficiently: Add and remove listeners as needed to prevent memory leaks
  3. Batch Operations: Use batch writes for multi-location updates
  4. Error Handling: Implement robust error handling for database operations
  5. Security First: Always consider security implications of data structure choices

Migration and Versioning

As the application evolves, the data model may need to change. The following approaches are used for data migration:

  1. Version Tracking: Including version fields in data objects
  2. Incremental Updates: Migrating data incrementally as users access it
  3. Background Jobs: Using Firebase Cloud Functions for bulk migrations
  4. Backward Compatibility: Maintaining support for older data structures during transitions