A full-stack real-time voice calling application built with WebRTC, featuring peer-to-peer audio communication, user authentication, and real-time presence management.
Live demo: webrtc-seven-alpha.vercel.app · API health: webrtc-74h2.onrender.com/health
The backend is on Render's free tier and spins down after ~15 minutes of inactivity — the first request after a nap can take up to 30s to wake it back up.
| Login | Dashboard |
|---|---|
![]() |
![]() |
| Incoming call | Active call |
|---|---|
![]() |
![]() |
Call screenshots were captured with two automated browser sessions placing a real call end-to-end (real signaling, ICE negotiation, and media tracks) — the green test pattern is Chromium's synthetic fake camera feed used for headless capture, not a rendering bug.
- Screenshots
- Application Overview
- Technology Stack
- System Architecture
- Project Structure
- Key Features
- Quick Start
- Documentation
- Architecture Diagrams
- Communication Flow
- Development Guide
This WebRTC VoIP application enables users to:
- Register and authenticate securely
- Discover other online users in real-time
- Initiate and receive voice calls using peer-to-peer WebRTC
- Accept, reject, or end calls
- Maintain call history and user presence status
The application follows a client-server architecture with peer-to-peer communication for media streaming:
- Server handles authentication, user management, signaling, and presence
- Clients communicate directly via WebRTC for audio/video without media passing through the server
| Layer | Technology |
|---|---|
| Runtime | Node.js v18+ |
| Framework | Express.js 5.x |
| Language | TypeScript 5.x |
| Database | MongoDB 9.x |
| Cache/Session | Redis 5.x |
| Real-time | Socket.IO 4.x |
| Authentication | JWT + Argon2 |
| Logging | Winston |
| Validation | Zod |
| Layer | Technology |
|---|---|
| Framework | Angular 21 |
| Language | TypeScript 5.x |
| Styling | Tailwind CSS 4.x |
| Components | DaisyUI 5.x |
| Real-time | Socket.IO Client 4.x |
| State | RxJS Observables |
| HTTP | Angular HttpClient |
| Routing | Angular Router |
- Containerization: Docker & Docker Compose
- Version Control: Git
- Package Manager: npm
- Build Tool: Angular CLI & TypeScript Compiler
┌─────────────────────────────────────────────────────────────┐
│ Frontend (Angular) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Login │ │ Dashboard │ │ Call UI │ │
│ │ Component │ │ Component │ │ Component │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Angular Services Layer │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────────────┐ │ │
│ │ │ Auth │ │ Call │ │ WebRTC Service │ │ │
│ │ │ Service │ │ Service │ │ (Peer Conn.) │ │ │
│ │ └─────────┘ └─────────┘ └─────────────────┘ │ │
│ │ ┌─────────────────────┐ ┌─────────────────┐ │ │
│ │ │ Socket Service │ │ HTTP Service │ │ │
│ │ │ (Real-time) │ │ (API calls) │ │ │
│ │ └─────────────────────┘ └─────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
└──────────────┬──────────────────────┬───────────────────────┘
│ │
HTTP/REST WebSocket (Socket.IO)
│ │
▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ Backend (Node.js/Express) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Authentication & Middleware Layer │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │ │
│ │ │ Auth │ │ Request │ │ Error Handling │ │ │
│ │ │Middleware│ │ ID │ │ & Validation │ │ │
│ │ └──────────┘ └──────────┘ └──────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ API Routes Layer │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │ │
│ │ │ /auth │ │ /calls │ │ /users │ │ │
│ │ │ Routes │ │ Routes │ │ Routes │ │ │
│ │ └──────────┘ └──────────┘ └──────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Service Layer │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │ │
│ │ │ User │ │ Call │ │ Signaling Service │ │ │
│ │ │ Service │ │ Service │ │ (WebRTC Signaling) │ │ │
│ │ └──────────┘ └──────────┘ └──────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Data Access & Integration Layer │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │ │
│ │ │MongoDB │ │ Redis │ │ Socket.IO Adapter │ │ │
│ │ │Database │ │ Cache │ │ (Pub/Sub) │ │ │
│ │ └──────────┘ └──────────┘ └──────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Logging & Monitoring │ │
│ │ ├─ Winston Logger with Daily Rotation │ │
│ │ ├─ Request ID Tracking │ │
│ │ └─ Error & Performance Monitoring │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
▲ ▲
│ │
REST API WebSocket (Signaling)
│ │
┌──────┴──────┬───────────────┴──────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌─────────────┐
│MongoDB │ │ Redis │ │Socket.IO │
│Database│ │ Cache │ │ Pub/Sub via │
│ │ │ │ │Redis Adapter│
└────────┘ └────────┘ └─────────────┘
┌──────────────────────────────────────┐ ┌──────────────────────────────────────┐
│ User A (Frontend) │ │ User B (Frontend) │
│ ┌────────────────────────────────┐ │ │ ┌────────────────────────────────┐ │
│ │ WebRTC Peer Connection │ │ │ │ WebRTC Peer Connection │ │
│ │ ┌──────────────────────────┐ │ │ │ │ ┌──────────────────────────┐ │ │
│ │ │ LocalStream (Microphone) │ │ │ │ │ │ LocalStream (Microphone) │ │ │
│ │ │ ┌──────────────────────┐ │ │ │ │ │ │ ┌──────────────────────┐ │ │ │
│ │ │ │ Audio Tracks (Raw) │ │ │ │ │ │ │ │ Audio Tracks (Raw) │ │ │ │
│ │ │ └──────────────────────┘ │ │ │ │ │ │ └──────────────────────┘ │ │ │
│ │ └──────────────────────────┘ │ │ │ │ └──────────────────────────┘ │ │
│ │ │ │ │ │ │ │ │ │
│ │ ▼ │ │ │ │ ▼ │ │
│ │ ┌──────────────────────────┐ │ │ │ │ ┌──────────────────────────┐ │ │
│ │ │ Add Tracks to Connection │ │ │ │ │ │ Add Tracks to Connection │ │ │
│ │ └──────────────────────────┘ │ │ │ │ └──────────────────────────┘ │ │
│ │ │ │ │ │ │ │ │ │
│ │ ▼ │ │ │ │ ▼ │ │
│ │ ┌──────────────────────────┐ │ │ │ │ ┌──────────────────────────┐ │ │
│ │ │ Codec & Encryption │ │ │ │ │ │ Codec & Encryption │ │ │
│ │ │ (OPUS, DTLS-SRTP) │ │ │ │ │ │ (OPUS, DTLS-SRTP) │ │ │
│ │ │ Remote Stream Reception │ │ │ │ │ │ Remote Stream Reception │ │ │
│ │ └──────────────────────────┘ │ │ │ │ └──────────────────────────┘ │ │
│ └────────────────────────────────┘ │ │ └────────────────────────────────┘ │
│ │ │ │
│ ▲ │ │ ▲ │
└──────────────┼────────────────────────┘ └──────────────────────┼────────────────┘
│ SRTP Media Stream │
│ (Encrypted Audio Data) │
│ │
└─────────────────────────────────────────────────────────┘
Direct P2P Connection (ICE/STUN/TURN)
Signaling Channel (via Socket.IO + Backend):
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 1. User A sends offer via Socket.IO → Backend → User B │
│ 2. User B sends answer via Socket.IO → Backend → User A │
│ 3. Both exchange ICE candidates via Socket.IO → Backend │
│ 4. Once connection established, media flows directly P2P │
└─────────────────────────────────────────────────────────────────────────────────┘
webrtc/
├── backend/ # Node.js/Express backend
│ ├── src/
│ │ ├── @types/ # Global type definitions
│ │ ├── config/ # Environment & configuration
│ │ ├── constants/ # Application constants
│ │ ├── exceptions/ # Custom exceptions
│ │ ├── libs/ # Core libraries
│ │ │ ├── database/ # MongoDB connection
│ │ │ ├── logger/ # Winston logger
│ │ │ ├── redis/ # Redis client
│ │ │ └── socket/ # Socket.IO server & signaling
│ │ ├── middlewares/ # Express middlewares
│ │ │ ├── auth.middleware.ts
│ │ │ ├── error.handler.middleware.ts
│ │ │ ├── request-id.middleware.ts
│ │ │ └── validation.schema.middleware.ts
│ │ ├── models/ # MongoDB schemas
│ │ │ ├── user.model.ts
│ │ │ └── call.model.ts
│ │ ├── routes/ # API routes & controllers
│ │ │ ├── auth/
│ │ │ ├── call/
│ │ │ └── users/
│ │ ├── types/ # TypeScript definitions
│ │ ├── utils/ # Utility functions
│ │ ├── app.ts # Express app setup
│ │ └── index.ts # Entry point
│ ├── Dockerfile
│ ├── docker-compose.yaml
│ ├── package.json
│ ├── tsconfig.json
│ ├── nodemon.json
│ └── README.md
│
├── frontend/ # Angular frontend
│ ├── src/
│ │ ├── app/
│ │ │ ├── components/ # Angular components
│ │ │ │ ├── login/ # Authentication UI
│ │ │ │ ├── dashboard/ # Main app interface
│ │ │ │ └── call/ # Call interface
│ │ │ ├── services/ # Angular services
│ │ │ │ ├── auth.ts # Authentication service
│ │ │ │ ├── call.ts # Call management
│ │ │ │ ├── socket.ts # Socket.IO client
│ │ │ │ └── webrtc.ts # WebRTC peer connection
│ │ │ ├── app.ts # Root component
│ │ │ ├── app.config.ts # App configuration
│ │ │ ├── app.routes.ts # Route definitions
│ │ │ └── styles.css # Global styles
│ │ ├── environments/ # Environment configs
│ │ ├── index.html
│ │ ├── main.ts
│ │ └── styles.css
│ ├── public/
│ ├── angular.json
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── package.json
│ └── README.md
│
├── SETUP.md # Setup and installation guide
└── README.md # This file
- Registration: Email, username, password
- Login: JWT-based token authentication, stored in an httpOnly cookie
- Session Management: Secure cookie handling
- Password Security: Argon2 hashing
- Online/offline status tracking
- Last seen timestamp
- Real-time user list updates via Socket.IO
- User profiles with avatars
- Signaling: Socket.IO for offer/answer exchange
- Media: Direct peer-to-peer audio streaming
- Codecs: OPUS audio codec for high-quality compression
- Security: DTLS-SRTP for encrypted media
- NAT Traversal: ICE, STUN, and TURN support
- Call Lifecycle: Initiate, accept, reject, end
- Call history tracking
- Call status monitoring
- Call duration tracking
- In-call notifications
- Missed call notifications
- Users: User profiles and authentication
- Calls: Call history and metadata
- Sessions: Redis for session management
- Socket.IO Adapter: Redis pub/sub for horizontal scaling
- CORS protection
- Helmet security headers
- HPP (HTTP Parameter Pollution) protection
- Input validation with Zod
- Request size limiting
- JWT expiry and refresh
- Structured logging with Winston
- Daily log rotation
- Request ID tracking for tracing
- Error logging and analytics
- Node.js v18+
- npm v11+
- MongoDB 6.0+
- Redis 7.0+
git clone <repository-url>
cd webrtc
# Backend setup
cd backend
cp .env.example .env # Configure environment variables
npm install
npm run dev
# Frontend setup (new terminal)
cd frontend
npm install
npm start- Frontend: http://localhost:4200
- Backend API: http://localhost:3000
- Register user 1: alice@example.com / alice123
- Register user 2: bob@example.com / bob123
- Login and start making calls!
- SETUP.md - Complete setup and installation guide
- backend/README.md - Backend architecture and API documentation
- frontend/README.md - Frontend architecture and component guide
| Topic | Location |
|---|---|
| Environment Setup | SETUP.md → Environment Setup |
| Backend Installation | SETUP.md → Backend Setup, backend/README.md |
| Frontend Installation | SETUP.md → Frontend Setup, frontend/README.md |
| API Endpoints | backend/README.md → API Endpoints |
| WebSocket Events | backend/README.md → WebSocket Events |
| Database Models | backend/README.md → Database Models |
| Components & Services | frontend/README.md → Application Architecture |
| Docker Deployment | SETUP.md → Docker Setup |
| Troubleshooting | SETUP.md → Troubleshooting |
1. Frontend Login Form
↓
2. POST /api/auth/login
├─ Email & Password
├─ Backend validates credentials
└─ Sets JWT as an httpOnly cookie (x-app-authorization), valid for 1 day
↓
3. Subsequent API/Socket.IO requests
├─ Browser sends the cookie automatically (withCredentials: true)
└─ Backend verifies the JWT and loads req.user from MongoDB
↓
4. On Cookie Expiry / Invalid Token
├─ Requests receive 401 Unauthorized
└─ Frontend redirects to /login (see authGuard)
1. User A clicks "Call User B"
↓
2. Frontend: GET /api/users/B (fetch user details)
↓
3. Frontend: Initialize WebRTC Peer Connection
├─ Request microphone permissions
└─ Get local audio stream
↓
4. Frontend: Create SDP Offer
├─ Offer encoded with local ICE candidates
└─ Offer sent to Backend via Socket.IO: "offer"
↓
5. Backend receives offer
├─ Validate sender & recipient
└─ Forward offer to User B via Socket.IO: "offer"
↓
6. User B Frontend receives offer
├─ Initialize Peer Connection
└─ Create SDP Answer
↓
7. User B sends answer to Backend
└─ Backend forwards to User A
↓
8. Both peers exchange ICE Candidates
├─ Via Socket.IO: "ice-candidate"
└─ Backend acts as signaling relay
↓
9. Connection Established
├─ Both RTCPeerConnection states: "connected"
└─ Media flows directly P2P (SRTP encrypted)
↓
10. Call Active
├─ Audio streams through WebRTC
└─ Call metadata stored in MongoDB via Backend
Browser A (User Login)
↓
Socket.IO Connect
↓
Backend: User joins Socket.IO room
├─ Update user.socketId in MongoDB
├─ Update user.isActive = true
└─ Update user.lastSeen = now
↓
Backend: Broadcast "user-joined" event
├─ Via Redis pub/sub to all instances
└─ To all connected browsers
↓
Browser B, C, D (Other users)
├─ Receive "user-joined" event
└─ Update online users list UI
↓
Browser A (User Logout/Disconnect)
├─ Socket.IO disconnect
└─ Backend updates user.isActive = false
↓
Backend: Broadcast "user-left" event
└─ To all connected browsers
Browser → Frontend Service → HTTP Request → Backend Route Handler
↓ ↓
├─ Authentication ├─ JWT Validation
├─ Error Handling ├─ Business Logic
└─ Response Display └─ Database Query
Browser ← Socket.IO Client ← Socket Event ← Backend Socket Server
↓ ↓
├─ Real-time Updates ├─ Broadcast to All
├─ Event Listeners ├─ Broadcast to User
└─ Instant Response └─ Broadcast to Room
Browser A RTCPeerConnection ←→ Network Stack ←→ Browser B RTCPeerConnection
├─ Microphone Audio (Direct P2P) ├─ Speaker Audio
├─ Codec (OPUS) ├─ Codec (OPUS)
├─ Encryption (DTLS-SRTP) (Encrypted) ├─ Decryption
└─ NAT Traversal (ICE) (via STUN/TURN) └─ NAT Traversal
Running in Development:
cd backend
npm run dev # Uses nodemon for auto-reloadAvailable Scripts:
npm run dev- Start with nodemon (development)npm run build- Compile TypeScriptnpm start- Run compiled code
Key Libraries:
- Express.js - HTTP server framework
- Socket.IO - Real-time bidirectional communication
- Mongoose - MongoDB object modeling
- IORedis - Redis client
- Winston - Logging library
- Zod - TypeScript-first schema validation
Running in Development:
cd frontend
npm start # Starts ng serve on port 4200Available Scripts:
npm start- Start dev servernpm run build- Production buildnpm run watch- Build in watch modenpm test- Run unit tests
Key Libraries:
- Angular 21 - Framework
- RxJS - Reactive programming
- Tailwind CSS - Utility CSS
- DaisyUI - Component library
- Socket.IO Client - Real-time communication
- Create controller in
backend/src/routes/[feature]/controllers/ - Add validation schema in
backend/src/routes/[feature]/validation/ - Create service method in service layer
- Add route in
backend/src/routes/[feature]/[feature].route.ts - Register route in
backend/src/routes/index.ts
cd frontend
ng generate component components/[component-name]- Create schema in
backend/src/models/[model].model.ts - Add Mongoose schema and interfaces
- Create service for business logic
- Expose via API routes
- Define event handler in
backend/src/libs/socket/signaling.service.ts - Emit event from Socket instance
- Add listener in
frontend/src/app/services/socket.ts - Handle event in component
- ✅ JWT tokens with expiry
- ✅ Argon2 password hashing
- ✅ CORS configured per environment
- ✅ Helmet security headers
- ✅ Input validation with Zod
- ✅ Request ID tracking
- ✅ Error messages don't leak sensitive info
- ✅ HTTPS enforced in production
- ✅ JWT tokens not stored in localStorage (uses memory)
- ✅ CORS properly configured
- ✅ XSS protection via Angular sanitization
- ✅ No sensitive data logged
- ✅ Secure WebRTC with DTLS-SRTP
- ✅ DTLS-SRTP encryption for media
- ✅ ICE to prevent address leakage
- ✅ TURN server for privacy
- ✅ No unencrypted STUN
- Redis caching for sessions
- MongoDB indexing on frequently queried fields
- Compression middleware
- Connection pooling
- Horizontal scaling via Socket.IO Redis adapter
- Lazy loading of routes
- Change detection optimization
- Tree-shaking of unused code
- Gzip compression
- CDN ready
- Adaptive bitrate for audio
- OPUS codec for efficient compression
- ICE candidate filtering
- Connection timeout handling
cd backend
docker-compose upStarts:
- Backend on :3000
- MongoDB on :27017
- Redis on :6379
- Set
NODE_ENV=production - Use strong JWT secrets
- Enable HTTPS
- Configure CORS for production domain
- Set database backups
- Enable monitoring and logging
- Configure firewall rules
- Use environment variables for secrets
- Set up rate limiting
- Configure reverse proxy (nginx)
WebRTC Connection Fails:
- Check firewall allows UDP
- Verify STUN/TURN configuration
- Check browser permissions
- Review backend logs
Call Audio Not Working:
- Check microphone permissions
- Verify media permissions in browser
- Check browser console for errors
- Test with different browser
Backend Won't Start:
- Check MongoDB is running
- Check Redis is running
- Verify
.envfile is configured - Check port 3000 is available
Frontend Won't Connect:
- Verify backend is running
- Check API URL in environment config
- Check CORS configuration
- Review browser console
See SETUP.md for detailed troubleshooting.
- Create a feature branch
- Make changes following code style guidelines
- Test thoroughly
- Submit pull request
- Request code review
ISC
Created as part of the km-learn WebRTC learning project.
Last Updated: January 15, 2026
Useful Resources:



