Discord-style group video/audio calls for 2–8 participants. WebRTC mesh topology, JWT auth, Next.js 14 + Node.js + Socket.IO.
- Node.js 20+
- npm 9+
npm install
cd backend && npm install
cd ../frontend && npm install
cd ..cp backend/.env.example backend/.envEdit backend/.env:
PORT=4000
DATABASE_URL="file:./dev.db"
JWT_SECRET=<generate below>
FRONTEND_ORIGIN=http://localhost:3000
Generate a secret (run one of these):
# Linux/macOS
openssl rand -hex 32
# Windows PowerShell
[System.Convert]::ToBase64String([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32))cp frontend/.env.local.example frontend/.env.localThe defaults work for local dev — no changes needed unless your backend is on a different port.
npm run db:push --prefix backendnpm run devBackend → http://localhost:4000 | Frontend → http://localhost:3000
- Open http://localhost:3000 in Window A. Click Register, create
alice@example.com/password123. - Type room ID
test-roomand click Join / Create Room. - Open http://localhost:3000 in Window B (incognito or different browser). Register
bob@example.com/password123. - Type the same room ID
test-roomand join. - Both windows show each other's video and hear audio within ~2 seconds. Toggle mic/cam — changes appear on the other side immediately. Click 📴 to leave.
| Button | Action |
|---|---|
| 🎙 / 🔇 | Toggle microphone — state propagates to all peers |
| 📹 / 📷 | Toggle camera — state propagates to all peers |
| 📴 | Leave the room |
STUN only — calls may fail behind symmetric NAT.
Uses Google's public STUN (stun:stun.l.google.com:19302). For cross-network calls (not same LAN/localhost), add a TURN server: Coturn or Twilio NTS. Add TURN credentials to frontend/src/lib/webrtc.ts.
HTTPS required for camera/microphone outside localhost.
getUserMedia only works on localhost without HTTPS. For any production deployment, serve the frontend over HTTPS.
Max 8 participants per room. Mesh topology: each user connects directly to every other user. Beyond 8, an SFU (mediasoup, LiveKit) would be needed.
JWTs expire after 7 days — no refresh tokens in v1.
realtime-call-system/
├── backend/
│ ├── prisma/schema.prisma # SQLite User model
│ ├── src/
│ │ ├── index.ts # HTTP + Socket.IO server boot
│ │ ├── env.ts # zod-validated env
│ │ ├── db.ts # Prisma client singleton
│ │ ├── auth/
│ │ │ ├── routes.ts # POST /auth/register, /auth/login, GET /auth/me
│ │ │ ├── jwt.ts # sign + verify JWT
│ │ │ └── middleware.ts # requireAuth Express middleware
│ │ └── rtc/
│ │ ├── gateway.ts # Socket.IO /rtc namespace + all event handlers
│ │ ├── rooms.ts # in-memory room map + helpers
│ │ └── events.ts # event name constants
│ ├── .env.example
│ ├── package.json
│ └── tsconfig.json
├── frontend/
│ ├── src/
│ │ ├── app/
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx # redirects to /login or /rooms
│ │ │ ├── globals.css
│ │ │ ├── login/page.tsx
│ │ │ ├── register/page.tsx
│ │ │ └── rooms/
│ │ │ ├── page.tsx # room id input
│ │ │ └── [roomId]/page.tsx # call screen + useCall logic
│ │ ├── lib/
│ │ │ ├── api.ts # fetch wrapper with JWT header
│ │ │ ├── socket.ts # socket.io-client singleton
│ │ │ └── webrtc.ts # PeerManager class
│ │ ├── stores/
│ │ │ ├── authStore.ts # zustand: user, token, login, logout
│ │ │ └── callStore.ts # zustand: peers, streams, mute states
│ │ └── components/
│ │ ├── VideoTile.tsx
│ │ ├── CallGrid.tsx
│ │ └── CallControls.tsx
│ ├── .env.local.example
│ ├── package.json
│ ├── tsconfig.json
│ ├── tailwind.config.ts
│ └── next.config.mjs
├── .gitignore
├── package.json # root: runs both via concurrently
└── README.md