Architecture

System Design

This section explains the overall architecture of the chat template, how data flows between Firebase and the frontend, and the design decisions that make it scalable and cost-effective.

Architecture Overview

The chat template follows a layered architecture that separates concerns between data persistence, business logic, and UI presentation. This design makes it easy to understand, maintain, and extend.

Layer Structure

1. Firebase Layer

Realtime Database: Fast, ephemeral state (presence, typing, room presence)

Firestore: Persistent data (users, rooms, messages)

2. Service Layer

Thin wrappers around Firebase SDK that handle subscriptions, writes, and data transformations. Located in features/chat/services.

3. Hook Layer

React hooks that connect services to components, manage subscriptions lifecycle, and provide loading/error states. Located in features/chat/hooks.

4. Component Layer

Presentational and container components that render the UI. Located in features/chat/components.

Data Flow Patterns

Reading Data (Subscriptions)

The template uses Firebase real-time listeners to keep the UI synchronized with the database:

1.
Component mounts → calls a hook (e.g. useRoomList)
2.
Hook → calls a service function (e.g. subscribeRoomsByUser)
3.
Service → sets up Firebase listener (e.g. onSnapshot) and returns unsubscribe function
4.
Data updates → Firebase calls callback → hook updates state → component re-renders
5.
Component unmounts → hook cleanup calls unsubscribe → listener removed

Writing Data

Writes are typically triggered by user actions and use Firebase batch operations when multiple documents need to be updated atomically:

1.
User action → component calls hook (e.g. useSendMessage)
2.
Hook → calls service function (e.g. sendMessage)
3.
Service → creates Firestore batch → writes message + updates room.lastMessage + increments unreadCounts → commits batch
4.
Firebase listeners → detect changes → subscribers receive updates → UI updates automatically

Design Decisions

Why Realtime Database for Presence & Typing?

Presence and typing indicators change very frequently (multiple times per second) and are ephemeral (they don't need to be stored long-term). Realtime Database is optimized for this use case:

  • Lower cost for high-frequency writes compared to Firestore
  • Built-in onDisconnect hooks for automatic cleanup
  • Faster updates with lower latency
  • No need for complex indexing or querying

Why Firestore for Messages & Rooms?

Messages and rooms need to be persisted, queried, and indexed:

  • Complex queries (e.g. "all rooms where I'm a participant, sorted by last message")
  • Pagination support for large message histories
  • Offline persistence and sync
  • Better security rules for fine-grained access control

Denormalization Strategy

The template denormalizes some data to optimize read performance:

  • rooms.lastMessage – avoids querying messages collection just to show preview
  • rooms.participantsCount – enables sorting without reading all participants
  • rooms.unreadCounts – per-user counters updated atomically with messages

Trade-off: These fields must be kept in sync when the source data changes (e.g. when sending a message, both the message and lastMessage are updated in a batch).

Subscription Management

All Firebase listeners are properly cleaned up to prevent memory leaks and unnecessary costs:

  • Hooks return cleanup functions that unsubscribe when components unmount
  • Services return unsubscribe functions that can be called explicitly
  • Presence sync tracks active subscriptions and removes unused ones automatically

Scalability Considerations

The template is designed to scale efficiently:

  • Efficient queries: Room list uses array-contains on participants array, which is indexed by default in Firestore
  • Sub-collections: Messages are stored as sub-collections under rooms, enabling efficient pagination and isolation
  • Selective subscriptions: Only active rooms and visible users have active listeners
  • Batch operations: Multiple writes are combined into single transactions to reduce costs
  • Client-side filtering: User search and presence tracking happen on the client to reduce Firestore reads