Why Real-Time Matters?
Real-time communication enhances user experience (UX), and real-time updates on what has happened. You would not want to reload to check if you have received a message right? Or check if you have received any notifications. Following are just a few basic examples of real-time communication:
- Chat Applications: Message b/w users, chatbots, etc.
- Live notifications: Alerts, Status Updates, etc.
Some complex applications for real-time:
- Online Multiplayer Games i.e. Xiangqi chess.
- Open-telemetry tools i.e. Prometheus, new relics where you get real-time updates on how your application is doing in terms of response time and traffic like RPM (requests per minute).
- Collaboration Tools: Simultaneous edits in a shared document.
- IoT Device Dashboards.
- Real-Time Dashboards and Analytics.
Web socket is a persistent, full-duplex communication channel between the client and server. Unlike HTTP, which is request-driven, WebSockets maintain a continuous connection, enabling real-time data exchange.
Choices in WebSocket Libraries
native WebSockets offer direct control, vs libraries like Socket.IO, SockJs offers the following:
- Automatic reconnection.
- Room-based communication for grouping clients.
- Compatibility fallbacks (e.g., using polling when WebSockets are unavailable).
Firebase for Real-Time Applications
Firebase, Google’s Backend-as-a-Service (BaaS) platform, offers a Realtime Database and Cloud Firestore, both of which support live synchronization of data across devices.
Why Firebase?
- Quick integration with mobile and web SDKs.
- A million connections can be created simultaneously without any issues.
- Ensures data availability even when offline.
Resource
Implement a Collaborative Document Editor
Real-time collaboration tools enable multiple users to work on the same document simultaneously. Let's move step-by-step to implement a collaborative document editor using Firebase for data persistence and WebSockets for real-time communication.
Why do we need to Combine Firebase and WebSockets?
- Firebase offers robust data persistence and synchronization across devices, with offline support and security rules.
- WebSockets provide the possibility of low-latency, event-driven communication for real-time collaboration.
- Combined together - Firebase guarantees stable and synchronous storage, and WebSockets offers instant updates to connected clients.
Features of the Collaborative Editor
- Real-time text synchronization across users.
- Change tracking with user attribution.
- Conflict resolution and offline support.
Step-by-Step Implementation
1. Setting Up the Project
Create a New Project Directory
mkdir collaborative-editor
cd collaborative-editor
Initialize a Node.js Project
npm init -y
Install Firebase SDK Inside the project directory:
npm install firebase
npm install -g http-server
Set Up the Directory Structure
mkdir client
2. Setting Up Firebase
Create a Firebase Project
- Go to the Firebase Console.
- Create a new project and enable Realtime Database or Cloud Firestore.
Initialize Firebase in Your Project
// client/firebaseConfig.js
import { initializeApp } from "https://www.gstatic.com/firebasejs/9.22.2/firebase-app.js";
import { getDatabase } from "https://www.gstatic.com/firebasejs/9.22.2/firebase-database.js";
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
databaseURL: "https://YOUR_PROJECT_ID.firebaseio.com",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_PROJECT_ID.appspot.com",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID"
};
const app = initializeApp(firebaseConfig);
export const db = getDatabase(app);
3. Building the Collaborative Editor Frontend
Create an HTML File Inside the client directory:
<!-- client/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Collaborative Editor</title>
</head>
<body>
<textarea id="editor" style="width: 100%; height: 80vh;"></textarea>
<script src="./index.js" type="module"></script>
</body>
</html>
Implement Real-Time Synchronization:
// client/index.js
import { db } from "./firebaseConfig.js";
import {
ref,
onValue,
set,
} from "https://www.gstatic.com/firebasejs/9.22.2/firebase-database.js";
const docRef = ref(db, "documents/doc1");
const textArea = document.getElementById("editor");
// Listen for Firebase updates
onValue(docRef, (snapshot) => {
const data = snapshot.val();
if (data && textArea.value !== data.text) {
textArea.value = data.text;
}
});
// Save text to Firebase
textArea.addEventListener('input', () => {
const text = textArea.value;
set(docRef, { text });
});
To run the implementation
cd client
http-server
You should be able to do the following at this point.
Let's enhance the UI a little bit and add interesting features.
Enhancing the UI
1. Update HTML file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Collaborative Editor</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f4f4f9;
}
header {
background-color: #6200ea;
color: white;
padding: 1rem;
text-align: center;
}
textarea {
width: 100%;
height: calc(80vh - 2rem);
border: none;
resize: none;
padding: 1rem;
font-size: 1rem;
background: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
outline: none;
margin: 1rem auto;
}
footer {
text-align: center;
padding: 1rem;
font-size: 0.9rem;
background-color: #6200ea;
color: white;
}
button {
margin: 0.5rem;
padding: 0.5rem 1rem;
background: #6200ea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background: #3700b3;
}
</style>
</head>
<body>
<header>
<h1>Collaborative Editor</h1>
</header>
<textarea id="editor" placeholder="Start typing..."></textarea>
<footer>
<button id="clearButton">Clear</button>
<button id="exportButton">Export</button>
</footer>
<script src="./index.js" type="module"></script>
</body>
</html>
2. Update index.js file
Clear Text and Export Text
// Clear text for all users
document.getElementById("clearButton").addEventListener("click", () => {
if (confirm("Are you sure you want to clear the document?")) {
set(docRef, { text: "" });
}
});
// Export text to a .txt file
document.getElementById("exportButton").addEventListener("click", () => {
const text = textArea.value;
const blob = new Blob([text], { type: "text/plain" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "document.txt";
link.click();
});
Add User Presence
Update firebaseConfig.js (new presence logic):
import { initializeApp } from "https://www.gstatic.com/firebasejs/9.22.2/firebase-app.js";
import { getDatabase, ref, onDisconnect, set } from "https://www.gstatic.com/firebasejs/9.22.2/firebase-database.js";
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
databaseURL: "https://YOUR_PROJECT_ID.firebaseio.com",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_PROJECT_ID.appspot.com",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID",
};
const app = initializeApp(firebaseConfig);
const db = getDatabase(app);
// User presence tracking
const userRef = ref(db, "users/" + Math.random().toString(36).substring(2));
set(userRef, true);
onDisconnect(userRef).remove();
export { db };
Update index.js:
// Updated imports
import { db } from "./firebaseConfig.js";
import {
ref,
onValue,
set,
onDisconnect,
get,
child,
} from "https://www.gstatic.com/firebasejs/9.22.2/firebase-database.js";
// User presence logic
const usersRef = ref(db, "users");
onValue(usersRef, (snapshot) => {
const users = snapshot.val() ? Object.keys(snapshot.val()).length : 0;
document.querySelector("header").innerHTML = `<h1>Collaborative Editor - ${users} Users Online</h1>`;
});
Add version History
New database structure:
{
"documents": {
"doc1": {
"text": "Current content",
"history": {
"timestamp1": "Previous content",
"timestamp2": "Another previous content"
}
}
}
}
Update index.js:
// Save version history
textArea.addEventListener("input", () => {
const text = textArea.value;
const timestamp = Date.now();
set(docRef, { text });
set(ref(db, `documents/doc1/history/${timestamp}`), text);
});
// Revert to previous version
function revertToVersion(timestamp) {
get(child(docRef, `history/${timestamp}`)).then((snapshot) => {
if (snapshot.exists()) {
set(docRef, { text: snapshot.val() });
}
});
}
Finally, here is how your implementation should look and work:
3. Handling Conflicts and Offline Support
Conflict Resolution
- Firebase automatically handles most synchronization issues. To further refine:
- Use timestamps or user IDs to track edits.
- Implement a merge strategy to reconcile conflicting updates.
Offline Mode
- Firebase automatically queues offline changes and syncs them when the connection is restored.
- Test offline functionality to ensure a seamless user experience.
Best Practices
Security
- Use Firebase security rules to restrict read/write access.
- An example rule to allow access only to authenticated users.
{
"rules": {
"documents": {
"$docId": {
".read": "auth != null",
".write": "auth != null"
}
}
}
}
Performance Optimization
- Minimize the size of data synced in real-time.
- Use database indexing for efficient querying.
Testing
- Simulate multi-user scenarios to test real-time updates.
- Validate offline support thoroughly.