Real-Time Application
Learning Objectives
- By the end of this project, you will be able to:
- - Implement WebSocket connections
- - Build real-time features
- - Design and develop backend APIs
- - Integrate databases with real-time updates
- - Handle concurrent connections
- - Optimize real-time performance
- - Deploy real-time applications
- - Handle edge cases and errors
Capstone Project 3: Real-Time Application
Project Overview
Build a production-ready Real-Time Application using WebSockets, combining frontend and backend with real-time communication. This capstone project will demonstrate mastery of WebSocket integration, real-time features, backend API development, database integration, and deployment.
Learning Objectives
By the end of this project, you will be able to:
- Implement WebSocket connections
- Build real-time features
- Design and develop backend APIs
- Integrate databases with real-time updates
- Handle concurrent connections
- Optimize real-time performance
- Deploy real-time applications
- Handle edge cases and errors
Project Requirements
Core Features
-
Real-Time Communication
- Instant message delivery
- Live updates
- User presence indicators
- Typing indicators
- Notifications
-
Backend API
- RESTful API endpoints
- WebSocket server
- Database integration
- Authentication
- Error handling
-
Database
- Store messages and data
- User management
- Real-time data sync
- Data persistence
-
User Features
- User authentication
- User profiles
- Online/offline status
- Message history
- Room/channel management
-
Advanced Features
- File sharing
- Message reactions
- Read receipts
- Message editing/deleting
- Search functionality
Technical Requirements
- Backend: Node.js, Express.js, Socket.io
- Frontend: React or Vue.js
- Database: MongoDB or PostgreSQL
- Authentication: JWT
- WebSockets: Socket.io
- Deployment: Backend and frontend deployment
Project Ideas
Choose one of these or create your own:
Option 1: Real-Time Collaboration Tool
- Multiple users editing documents
- Live cursor positions
- Change tracking
- Comments and suggestions
- Version history
Option 2: Live Dashboard Application
- Real-time data visualization
- Multiple data sources
- Live updates
- Customizable widgets
- Alerts and notifications
Option 3: Multiplayer Game
- Real-time game state
- Player interactions
- Leaderboards
- Game rooms
- Chat system
Option 4: Live Polling/Voting App
- Create polls
- Real-time vote counting
- Results visualization
- Multiple poll types
- Admin controls
Option 5: Team Communication Platform
- Channels and direct messages
- File sharing
- Video/audio calls (optional)
- Notifications
- Message threads
Recommended Project: Real-Time Collaboration Tool
We'll use a Real-Time Collaboration Tool as our example. You can adapt this to any of the options above.
Project Structure
collaboration-app/
├── backend/
│ ├── src/
│ │ ├── config/
│ │ │ └── database.js
│ │ ├── models/
│ │ │ ├── User.js
│ │ │ ├── Document.js
│ │ │ └── Change.js
│ │ ├── controllers/
│ │ │ ├── authController.js
│ │ │ ├── documentController.js
│ │ │ └── userController.js
│ │ ├── routes/
│ │ │ ├── authRoutes.js
│ │ │ ├── documentRoutes.js
│ │ │ └── userRoutes.js
│ │ ├── socket/
│ │ │ ├── socketHandlers.js
│ │ │ └── documentHandlers.js
│ │ ├── middleware/
│ │ │ ├── auth.js
│ │ │ └── errorHandler.js
│ │ └── server.js
│ ├── .env
│ └── package.json
├── frontend/
│ ├── src/
│ │ ├── components/
│ │ │ ├── Layout/
│ │ │ ├── Document/
│ │ │ ├── Editor/
│ │ │ └── UserList/
│ │ ├── pages/
│ │ │ ├── Home.jsx
│ │ │ ├── DocumentPage.jsx
│ │ │ └── Dashboard.jsx
│ │ ├── context/
│ │ │ ├── AuthContext.jsx
│ │ │ └── SocketContext.jsx
│ │ ├── hooks/
│ │ │ └── useSocket.js
│ │ ├── services/
│ │ │ └── api.js
│ │ └── App.jsx
│ └── package.json
└── README.md
Step-by-Step Implementation
Phase 1: Backend Setup
# Create backend directory
mkdir collaboration-backend
cd collaboration-backend
npm init -y
# Install dependencies
npm install express socket.io mongoose dotenv bcrypt jsonwebtoken cors helmet express-rate-limit
npm install --save-dev nodemon
# Create project structure
mkdir -p src/{config,models,controllers,routes,socket,middleware}
Phase 2: Database Models
// backend/src/models/Document.js
const mongoose = require('mongoose');
const changeSchema = new mongoose.Schema({
type: {
type: String,
enum: ['insert', 'delete', 'format'],
required: true
},
position: {
type: Number,
required: true
},
text: String,
length: Number,
format: {
type: Map,
of: String
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
timestamp: {
type: Date,
default: Date.now
}
}, { _id: false });
const documentSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true
},
content: {
type: String,
default: ''
},
owner: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
collaborators: [{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
role: {
type: String,
enum: ['viewer', 'editor'],
default: 'editor'
}
}],
changes: [changeSchema],
version: {
type: Number,
default: 1
},
isPublic: {
type: Boolean,
default: false
}
}, {
timestamps: true
});
module.exports = mongoose.model('Document', documentSchema);
Phase 3: Socket Handlers
// backend/src/socket/documentHandlers.js
const Document = require('../models/Document');
const User = require('../models/User');
// Store active users per document
const activeUsers = new Map();
function setupDocumentHandlers(io, socket) {
// Join document room
socket.on('document:join', async (data) => {
const { documentId, userId } = data;
try {
const document = await Document.findById(documentId);
if (!document) {
socket.emit('error', { message: 'Document not found' });
return;
}
// Check access
const hasAccess = document.owner.toString() === userId ||
document.collaborators.some(c => c.user.toString() === userId) ||
document.isPublic;
if (!hasAccess) {
socket.emit('error', { message: 'Access denied' });
return;
}
// Join room
socket.join(documentId);
// Track active user
if (!activeUsers.has(documentId)) {
activeUsers.set(documentId, new Map());
}
const user = await User.findById(userId).select('name email');
activeUsers.get(documentId).set(socket.id, {
userId,
name: user.name,
cursor: null
});
// Notify others
socket.to(documentId).emit('user:joined', {
userId,
name: user.name
});
// Send current document state
socket.emit('document:state', {
content: document.content,
version: document.version
});
// Send active users
const users = Array.from(activeUsers.get(documentId).values());
io.to(documentId).emit('users:list', users);
} catch (error) {
socket.emit('error', { message: error.message });
}
});
// Handle text changes
socket.on('document:change', async (data) => {
const { documentId, change, userId } = data;
try {
const document = await Document.findById(documentId);
if (!document) return;
// Apply change to content
let newContent = document.content;
if (change.type === 'insert') {
newContent = newContent.slice(0, change.position) +
change.text +
newContent.slice(change.position);
} else if (change.type === 'delete') {
newContent = newContent.slice(0, change.position) +
newContent.slice(change.position + change.length);
}
// Save change
document.content = newContent;
document.changes.push({
...change,
userId
});
document.version += 1;
await document.save();
// Broadcast to other users in room
socket.to(documentId).emit('document:change', {
change,
userId,
version: document.version
});
} catch (error) {
socket.emit('error', { message: error.message });
}
});
// Handle cursor position
socket.on('cursor:update', (data) => {
const { documentId, position, userId } = data;
if (activeUsers.has(documentId)) {
const user = activeUsers.get(documentId).get(socket.id);
if (user) {
user.cursor = position;
socket.to(documentId).emit('cursor:update', {
userId,
position,
name: user.name
});
}
}
});
// Handle user leaving
socket.on('document:leave', (data) => {
const { documentId, userId } = data;
if (activeUsers.has(documentId)) {
activeUsers.get(documentId).delete(socket.id);
if (activeUsers.get(documentId).size === 0) {
activeUsers.delete(documentId);
} else {
socket.to(documentId).emit('user:left', { userId });
const users = Array.from(activeUsers.get(documentId).values());
io.to(documentId).emit('users:list', users);
}
}
});
// Cleanup on disconnect
socket.on('disconnect', () => {
activeUsers.forEach((users, documentId) => {
if (users.has(socket.id)) {
const user = users.get(socket.id);
users.delete(socket.id);
if (users.size === 0) {
activeUsers.delete(documentId);
} else {
io.to(documentId).emit('user:left', { userId: user.userId });
const userList = Array.from(users.values());
io.to(documentId).emit('users:list', userList);
}
}
});
});
}
module.exports = setupDocumentHandlers;
Phase 4: Socket Server Setup
// backend/src/socket/socketHandlers.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const setupDocumentHandlers = require('./documentHandlers');
function setupSocketIO(io) {
// Authentication middleware for Socket.io
io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('Authentication error'));
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.userId).select('-password');
if (!user) {
return next(new Error('User not found'));
}
socket.userId = user._id.toString();
socket.user = user;
next();
} catch (error) {
next(new Error('Authentication error'));
}
});
io.on('connection', (socket) => {
console.log('User connected:', socket.userId);
// Setup document handlers
setupDocumentHandlers(io, socket);
socket.on('disconnect', () => {
console.log('User disconnected:', socket.userId);
});
});
}
module.exports = setupSocketIO;
Phase 5: Server with Socket.io
// backend/src/server.js
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
require('dotenv').config();
const authRoutes = require('./routes/authRoutes');
const documentRoutes = require('./routes/documentRoutes');
const errorHandler = require('./middleware/errorHandler');
const setupSocketIO = require('./socket/socketHandlers');
const app = express();
const server = http.createServer(app);
// Configure Socket.io
const io = new Server(server, {
cors: {
origin: process.env.CLIENT_URL || 'http://localhost:3000',
methods: ['GET', 'POST'],
credentials: true
}
});
// Setup Socket.io handlers
setupSocketIO(io);
// Middleware
app.use(helmet());
app.use(cors({
origin: process.env.CLIENT_URL || 'http://localhost:3000',
credentials: true
}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/documents', documentRoutes);
// Health check
app.get('/api/health', (req, res) => {
res.json({
success: true,
message: 'API is running',
timestamp: new Date().toISOString()
});
});
// Error handler
app.use(errorHandler);
// Connect to database
mongoose.connect(process.env.MONGODB_URI)
.then(() => {
console.log('Connected to MongoDB');
const PORT = process.env.PORT || 5000;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
})
.catch((error) => {
console.error('MongoDB connection error:', error);
process.exit(1);
});
Phase 6: Frontend Socket Context
// frontend/src/context/SocketContext.jsx
import { createContext, useContext, useEffect, useState } from 'react';
import { io } from 'socket.io-client';
import { useAuth } from './AuthContext';
const SocketContext = createContext();
export function SocketProvider({ children }) {
const { user, isAuthenticated } = useAuth();
const [socket, setSocket] = useState(null);
const [connected, setConnected] = useState(false);
useEffect(() => {
if (isAuthenticated && user) {
const token = localStorage.getItem('token');
const newSocket = io(process.env.REACT_APP_SOCKET_URL || 'http://localhost:5000', {
auth: {
token
},
transports: ['websocket']
});
newSocket.on('connect', () => {
console.log('Socket connected');
setConnected(true);
});
newSocket.on('disconnect', () => {
console.log('Socket disconnected');
setConnected(false);
});
newSocket.on('error', (error) => {
console.error('Socket error:', error);
});
setSocket(newSocket);
return () => {
newSocket.close();
};
}
}, [isAuthenticated, user]);
return (
<SocketContext.Provider value={{ socket, connected }}>
{children}
</SocketContext.Provider>
);
}
export function useSocket() {
const context = useContext(SocketContext);
if (!context) {
throw new Error('useSocket must be used within SocketProvider');
}
return context;
}
Phase 7: Document Editor Component
// frontend/src/components/Editor/DocumentEditor.jsx
import { useState, useEffect, useRef } from 'react';
import { useSocket } from '../../context/SocketContext';
import { useAuth } from '../../context/AuthContext';
import './DocumentEditor.css';
function DocumentEditor({ documentId, initialContent }) {
const { socket } = useSocket();
const { user } = useAuth();
const [content, setContent] = useState(initialContent || '');
const [version, setVersion] = useState(1);
const [activeUsers, setActiveUsers] = useState([]);
const [cursors, setCursors] = useState(new Map());
const editorRef = useRef(null);
const lastChangeRef = useRef(null);
useEffect(() => {
if (!socket || !documentId) return;
// Join document
socket.emit('document:join', {
documentId,
userId: user.id
});
// Listen for document state
socket.on('document:state', (data) => {
setContent(data.content);
setVersion(data.version);
});
// Listen for changes from others
socket.on('document:change', (data) => {
if (data.userId !== user.id) {
applyChange(data.change);
setVersion(data.version);
}
});
// Listen for active users
socket.on('users:list', (users) => {
setActiveUsers(users);
});
// Listen for cursor updates
socket.on('cursor:update', (data) => {
if (data.userId !== user.id) {
setCursors(prev => {
const newCursors = new Map(prev);
newCursors.set(data.userId, {
position: data.position,
name: data.name
});
return newCursors;
});
}
});
// Listen for user joined/left
socket.on('user:joined', (data) => {
console.log(`${data.name} joined`);
});
socket.on('user:left', (data) => {
setCursors(prev => {
const newCursors = new Map(prev);
newCursors.delete(data.userId);
return newCursors;
});
});
return () => {
socket.emit('document:leave', { documentId, userId: user.id });
socket.off('document:state');
socket.off('document:change');
socket.off('users:list');
socket.off('cursor:update');
socket.off('user:joined');
socket.off('user:left');
};
}, [socket, documentId, user]);
const applyChange = (change) => {
setContent(prev => {
if (change.type === 'insert') {
return prev.slice(0, change.position) +
change.text +
prev.slice(change.position);
} else if (change.type === 'delete') {
return prev.slice(0, change.position) +
prev.slice(change.position + change.length);
}
return prev;
});
};
const handleChange = (e) => {
const newContent = e.target.value;
const oldContent = content;
const position = e.target.selectionStart;
// Calculate change
let change;
if (newContent.length > oldContent.length) {
// Insert
const insertedText = newContent.slice(position - (newContent.length - oldContent.length), position);
change = {
type: 'insert',
position: position - insertedText.length,
text: insertedText
};
} else {
// Delete
change = {
type: 'delete',
position,
length: oldContent.length - newContent.length
};
}
// Update local state
setContent(newContent);
// Debounce and send change
if (lastChangeRef.current) {
clearTimeout(lastChangeRef.current);
}
lastChangeRef.current = setTimeout(() => {
if (socket && documentId) {
socket.emit('document:change', {
documentId,
change,
userId: user.id
});
}
}, 100);
// Update cursor position
if (socket && documentId) {
socket.emit('cursor:update', {
documentId,
position: e.target.selectionStart,
userId: user.id
});
}
};
const handleSelectionChange = (e) => {
if (socket && documentId) {
socket.emit('cursor:update', {
documentId,
position: e.target.selectionStart,
userId: user.id
});
}
};
return (
<div className="document-editor">
<div className="editor-header">
<div className="active-users">
{activeUsers.map(u => (
<span key={u.userId} className="user-badge">
{u.name}
</span>
))}
</div>
<div className="editor-version">
Version: {version}
</div>
</div>
<textarea
ref={editorRef}
value={content}
onChange={handleChange}
onSelect={handleSelectionChange}
className="editor-textarea"
placeholder="Start typing..."
/>
<div className="cursors-overlay">
{Array.from(cursors.entries()).map(([userId, cursor]) => (
<div
key={userId}
className="cursor-indicator"
style={{ left: `${cursor.position * 8}px` }}
>
<span className="cursor-name">{cursor.name}</span>
<div className="cursor-line"></div>
</div>
))}
</div>
</div>
);
}
export default DocumentEditor;
Phase 8: Document Page
// frontend/src/pages/DocumentPage.jsx
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { documentsAPI } from '../services/api';
import { useAuth } from '../context/AuthContext';
import DocumentEditor from '../components/Editor/DocumentEditor';
import Loading from '../components/Common/Loading';
import Error from '../components/Common/Error';
import './DocumentPage.css';
function DocumentPage() {
const { id } = useParams();
const navigate = useNavigate();
const { user } = useAuth();
const [document, setDocument] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
loadDocument();
}, [id]);
const loadDocument = async () => {
try {
setLoading(true);
const response = await documentsAPI.getDocument(id);
setDocument(response.data.data);
} catch (err) {
setError(err.response?.data?.error || 'Failed to load document');
} finally {
setLoading(false);
}
};
if (loading) {
return <Loading />;
}
if (error) {
return <Error message={error} />;
}
if (!document) {
return <Error message="Document not found" />;
}
return (
<div className="document-page">
<div className="document-header">
<button onClick={() => navigate(-1)} className="btn btn-back">
← Back
</button>
<h1>{document.title}</h1>
<div className="document-actions">
<button className="btn btn-secondary">Share</button>
<button className="btn btn-secondary">Export</button>
</div>
</div>
<DocumentEditor
documentId={id}
initialContent={document.content}
/>
</div>
);
}
export default DocumentPage;
Phase 9: App Component
// frontend/src/App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import { SocketProvider } from './context/SocketContext';
import Layout from './components/Layout/Layout';
import Home from './pages/Home';
import Login from './pages/Login';
import Register from './pages/Register';
import Dashboard from './pages/Dashboard';
import DocumentPage from './pages/DocumentPage';
import ProtectedRoute from './components/ProtectedRoute';
import './App.css';
function App() {
return (
<AuthProvider>
<SocketProvider>
<BrowserRouter>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/document/:id"
element={
<ProtectedRoute>
<DocumentPage />
</ProtectedRoute>
}
/>
</Routes>
</Layout>
</BrowserRouter>
</SocketProvider>
</AuthProvider>
);
}
export default App;
Features Implementation
WebSocket Integration
- Socket.io Server: Real-time server setup
- Socket.io Client: Frontend connection
- Authentication: JWT-based socket auth
- Room Management: Document rooms
Real-Time Features
- Live Editing: Collaborative text editing
- Cursor Tracking: See other users' cursors
- User Presence: Show active users
- Change Broadcasting: Instant updates
- Version Control: Track document versions
Backend API
- REST Endpoints: Document CRUD
- WebSocket Events: Real-time events
- Database: MongoDB integration
- Authentication: Secure access
Database
- Document Storage: Store documents
- Change History: Track all changes
- User Management: User data
- Relationships: User-document relationships
Testing Your Application
Manual Testing Checklist
- [ ] Connect multiple users
- [ ] Real-time text editing
- [ ] See other users' cursors
- [ ] User join/leave notifications
- [ ] Document persistence
- [ ] Change history
- [ ] Error handling
- [ ] Reconnection handling
- [ ] Performance with many users
- [ ] Mobile responsiveness
Deployment
Backend Deployment
# Heroku example
heroku create your-app-backend
heroku config:set MONGODB_URI=your-uri
heroku config:set JWT_SECRET=your-secret
heroku config:set CLIENT_URL=https://your-frontend.com
git push heroku main
Frontend Deployment
# Vercel example
npm run build
vercel
vercel env add REACT_APP_SOCKET_URL
vercel env add REACT_APP_API_URL
Environment Variables
Backend (.env):
NODE_ENV=production
PORT=5000
MONGODB_URI=your-mongodb-uri
JWT_SECRET=your-secret-key
CLIENT_URL=https://your-frontend.com
Frontend (.env):
REACT_APP_API_URL=https://your-backend.com/api
REACT_APP_SOCKET_URL=https://your-backend.com
Exercise: Real-Time Application
Instructions:
- Choose your project idea
- Set up backend with Socket.io
- Set up frontend
- Implement real-time features
- Test with multiple users
- Deploy to production
Timeline: 3-4 weeks recommended
Common Issues and Solutions
Issue: Socket connection fails
Solution: Check CORS configuration and authentication token.
Issue: Changes not syncing
Solution: Verify event names match and changes are being saved.
Issue: Performance issues
Solution: Implement debouncing, optimize change broadcasting.
Quiz: WebSockets
-
WebSockets:
- A) Real-time bidirectional communication
- B) One-way communication
- C) Both
- D) Neither
-
Socket.io:
- A) WebSocket library
- B) HTTP library
- C) Both
- D) Neither
-
Real-time features:
- A) Instant updates
- B) Delayed updates
- C) Both
- D) Neither
-
Rooms:
- A) Group connections
- B) Don't group connections
- C) Both
- D) Neither
-
Database:
- A) Persists real-time data
- B) Doesn't persist data
- C) Both
- D) Neither
Answers:
- A) Real-time bidirectional communication
- A) WebSocket library
- A) Instant updates
- A) Group connections
- A) Persists real-time data
Key Takeaways
- WebSockets: Enable real-time communication
- Socket.io: Simplifies WebSocket implementation
- Real-Time Features: Instant updates and collaboration
- Backend API: REST + WebSocket combination
- Database: Persist real-time data
- Best Practice: Handle connections, errors, and performance
Next Steps
Congratulations! You've built a Real-Time Application. You now know:
- How to implement WebSockets
- How to build real-time features
- How to integrate backend APIs
- How to work with databases
- How to deploy real-time apps
What's Next?
- Assessment and Certification
- Complete all projects
- Pass assessments
- Get certified
Capstone Project completed! You've demonstrated mastery of real-time application development!