A full-stack, production-deployed peer-to-peer video calling app built completely from scratch. Real-time video communication powered by raw WebRTC and Socket.io — without third-party video SDKs.
Vcall is a real-time peer-to-peer video calling application built using raw WebRTC and Socket.io signaling. The project focuses on understanding low-level realtime communication architecture rather than relying on third-party video platforms.
The application supports direct browser-to-browser audio/video streaming, authentication, screen sharing, live online presence, and responsive media controls while handling real-world deployment challenges in production.
- Built using raw WebRTC APIs without Zoom, Agora, Twilio, or other video SDKs
- Implemented complete Socket.io signaling architecture from scratch
- Integrated secure authentication using Passport.js + bcrypt + PostgreSQL sessions
- Solved a real production deployment issue involving WebSocket proxy limitations on Vercel
- Implemented screen sharing with seamless camera restoration using live track replacement
- Handled ICE candidate timing issues with a pending candidate queue
- Deployed full-stack architecture using Vercel + Render + Neon PostgreSQL
| Feature | Details | |
|---|---|---|
| 🎥 | 1-on-1 Video Calling | Direct P2P communication using RTCPeerConnection |
| 👥 | Live Online Presence | Realtime online users powered by Socket.io |
| 🖥️ | Screen Sharing | Replace live camera stream without dropping the call |
| 🎙️ | Media Controls | Toggle microphone and camera during calls |
| 🔐 | Authentication System | Register/login with bcrypt + Passport.js |
| 💾 | Persistent Sessions | PostgreSQL-backed session storage |
| 📱 | Responsive Design | Optimized layout for desktop and mobile devices |
| ⚡ | Realtime Signaling | Offer/answer and ICE exchange using Socket.io |
| 🔔 | Call Notifications | Incoming call alerts, rejections, and hangups |
┌──────────────────────────────────────────────────────────────┐
│ BROWSER (User A) │
│ React · WebRTC · Socket.io-client │
└──────────────┬───────────────────────┬───────────────────────┘
│ REST (HTTPS) │ Socket.io (WSS)
▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ Render Backend (Node.js + Express) │
│ │
│ ┌──────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ Passport.js │ │ Socket.io │ │ Neon Postgres │ │
│ │ Auth + API │ │ Signaling │ │ Sessions │ │
│ └──────────────┘ └───────────────┘ └───────────────┘ │
└──────────────────────────────────────────────────────────────┘
│ Offer / Answer / ICE (signaling only)
▼
┌──────────────────────────────────────────────────────────────┐
│ BROWSER (User B) │
│ React · WebRTC · Socket.io-client │
└──────────────────────────┬───────────────────────────────────┘
│
◄════════════════╧═══════════════════►
Direct P2P WebRTC Stream
(audio/video bypasses server entirely)User A Socket.io Server User B
│ │ │
│── register("alice") ─────►│ │
│ │◄──── register("bob") ───────│
│◄── online-users({...}) ───┼──── online-users({...}) ───►│
│ │ │
│── offer (SDP) ───────────►│──────── offer (SDP) ───────►│
│◄─────────── answer (SDP) ─│◄──────── answer (SDP) ──────│
│── ICE candidate ─────────►│──────── ICE candidate ─────►│
│◄─────────── ICE candidate─│◄──────── ICE candidate ─────│
│ │ │
│◄════════════ Direct P2P Video/Audio ═══════════════════►│WebRTC peers use STUN servers to discover public network addresses and establish direct peer-to-peer connections.
Current configuration:
const peerConnection = new RTCPeerConnection({
iceServers: [
{
urls: "stun:stun.l.google.com:19302"
}
]
});Currently the project uses public STUN servers only. TURN servers are not configured yet, so connectivity may fail on highly restrictive networks.
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | React 18, React Router v6 | Single-page application |
| Styling | Tailwind CSS, Lucide Icons | Responsive UI |
| Realtime | Socket.io | Bidirectional realtime signaling |
| Video/Audio | WebRTC RTCPeerConnection |
Native browser P2P communication |
| Backend | Node.js, Express | REST API + signaling server |
| Auth | Passport.js, bcrypt | Secure authentication |
| Sessions | express-session, connect-pg-simple | Persistent sessions |
| Database | Neon PostgreSQL | User and session storage |
| Frontend Deploy | Vercel | Frontend hosting |
| Backend Deploy | Render | Node.js backend hosting |
- Node.js v18+
- PostgreSQL
git clone https://github.com/vansh1011/vcall.git
cd vcallcd backend
npm installCreate backend/.env:
PORT=8000
NODE_ENV=development
DATABASE_URL=your_database_connection_string
SECRET=your_session_secret
FRONTURL=http://localhost:5173Create required tables:
CREATE TABLE reg (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password TEXT NOT NULL
);
CREATE TABLE session (
sid VARCHAR PRIMARY KEY,
sess JSON NOT NULL,
expire TIMESTAMP(6) NOT NULL
);Start backend:
npm startcd frontend
npm installCreate frontend/.env:
VITE_API_URL=http://localhost:8000Run frontend:
npm run devOpen:
http://localhost:5173Register two accounts in separate tabs and start a call.
- Create a new Web Service on Render
- Configure environment variables:
DATABASE_URL=your_database_url
SECRET=your_secret
FRONTURL=https://your-app.vercel.app
NODE_ENV=production- Import frontend repository into Vercel
- Configure environment variable:
VITE_API_URL=your_backend_url- Add
vercel.json:
{
"rewrites": [
{
"source": "/api/:path*",
"destination": "https://your-backend-url/:path*"
}
]
}Vercel serverless rewrites do not properly support HTTP → WebSocket upgrades. REST APIs worked correctly through rewrites, but Socket.io connections silently failed.
Solution:
- REST requests continue through the Vercel proxy
- Socket.io connects directly to the backend origin
This separation solved realtime connection instability in production.
Implemented live media track replacement using:
RTCRtpSender.replaceTrack()This allows switching between camera and screen share without renegotiating the entire peer connection.
Remote ICE candidates may arrive before:
setRemoteDescription()is completed.
To prevent errors, candidates are temporarily stored inside a pending queue and flushed once the remote description becomes available.
Instead of storing sessions in memory, sessions are stored in PostgreSQL using:
connect-pg-simpleThis prevents users from being logged out whenever the backend restarts.
The primary goal of this project was to deeply understand realtime communication internals including:
- SDP negotiation
- ICE candidate exchange
- NAT traversal
- Peer connection lifecycle
- Realtime signaling architecture
- Media stream management
Using raw WebRTC APIs provided full control over the connection flow and exposed real-world networking and deployment challenges directly.
- Understanding WebRTC peer connection lifecycle deeply
- Handling asynchronous realtime communication reliably
- Debugging ICE candidate timing issues
- Managing media streams and live track replacement
- Deploying realtime applications in production environments
- Working with persistent session storage using PostgreSQL
- Understanding WebSocket deployment limitations on serverless platforms
vcall/
├── backend/
│ ├── server.js # REST API + Socket.io signaling server
│ ├── db.js # PostgreSQL pool configuration
│ └── .env
│
└── frontend/
├── src/
│ ├── pages/
│ │ ├── Home.jsx
│ │ ├── Call.jsx
│ │ ├── Login.jsx
│ │ └── Register.jsx
│ ├── socket.js
│ └── api.js
├── vercel.json
└── .env- Group video calling
- TURN server integration
- In-call chat messaging
- File sharing
- Call recording
- Better reconnection handling
- End-to-end encryption enhancements
PORT=8000
NODE_ENV=development
DATABASE_URL=your_database_url
SECRET=your_secret
FRONTURL=http://localhost:5173VITE_API_URL=http://localhost:8000Vansh
- GitHub: https://github.com/vansh1011
- Project: https://github.com/vansh1011/vcall
- Live Demo: https://vcall-drab.vercel.app/



