A beginner-friendly real-time voice chat app built with plain HTML, CSS, JavaScript, WebRTC, Node.js, and Socket.IO.
The app now creates shareable room IDs automatically, exposes a copyable invite link, and can serve the frontend directly from the Node server so one Render deployment can host the full experience.
.
├── .github/
│ └── workflows/
│ └── deploy-client.yml
├── client/
│ ├── index.html
│ ├── script.js
│ └── style.css
├── server/
│ ├── package.json
│ └── server.js
└── README.md
- The browser gets microphone access with
getUserMedia. - The Socket.IO server helps users in the same room exchange WebRTC offers, answers, and ICE candidates.
- After signaling is complete, the audio stream flows directly between browsers with WebRTC.
- This project uses a simple mesh setup, so it is best for small rooms.
- This demo uses public STUN servers by default. For stronger real-world reliability, add a TURN server later.
-
Install backend dependencies:
cd server npm install npm run dev -
In a second terminal, start a simple static server for the frontend:
cd client python3 -m http.server 5500 -
Open http://127.0.0.1:5500 in two browser tabs or on two devices.
-
Share the generated invite link or join the same room ID in both tabs.
-
Allow microphone access when the browser asks.
-
If a browser blocks autoplay, press the Play button on that participant card.
Note: opening index.html directly with file:// is not recommended because microphone access works best on localhost or HTTPS.
This is now the simplest deployment path because Render can host the Socket.IO backend and the static frontend from the same URL.
-
Create a new GitHub repository and push this project.
-
Create a new Web Service on Render.
-
Point Render to your GitHub repository.
-
Set the Root Directory to
server. -
Use:
- Build command:
npm install - Start command:
npm start
- Build command:
-
Deploy the service and open the Render URL, for example:
https://voice-chat-7ryk.onrender.com
-
Optional environment variables:
CLIENT_ORIGIN=https://YOUR_GITHUB_USERNAME.github.io,http://127.0.0.1:5500,http://localhost:5500TURN_SERVER_URLS=turn:YOUR_TURN_HOST:3478TURN_USERNAME=YOUR_USERNAMETURN_CREDENTIAL=YOUR_PASSWORD
If your users join from different mobile data/Wi-Fi networks, these TURN variables are not really optional for reliable audio. STUN alone often works only when both peers can reach each other directly.
When the app is served from Render, the frontend automatically connects back to the same origin, so you do not need to hard-code the backend URL anymore.
GitHub Pages is still optional if you want a separate static frontend.
- Open
client/config.js. - Set
signalingServerUrlto your live backend, for examplehttps://voice-chat-7ryk.onrender.com. - If calls should work across different networks, also set
rtcConfiguration.iceServerswith a TURN server inclient/config.js. - Commit and push your changes to GitHub.
- In your GitHub repository, open Settings > Pages and set the source to GitHub Actions.
- Push to the
mainbranch. - The included workflow publishes the
client/folder to GitHub Pages automatically.
Your site URL will look like this:
https://YOUR_GITHUB_USERNAME.github.io/YOUR_REPOSITORY_NAME/
If your default branch is not main, update .github/workflows/deploy-client.yml.
- A user joins a room from the frontend.
- The frontend connects to the Socket.IO server and sends
join-room. - The server returns the list of people already in that room.
- The new user creates a WebRTC offer for each existing user.
- Existing users create answers and send them back through the server.
- Both sides exchange ICE candidates through the server.
- Once WebRTC finishes connecting, audio travels directly between browsers.
The Socket.IO server only handles signaling messages. The voice audio itself does not pass through the server after the peer connection is established.
- If the room connects but you still cannot hear the other person, first press Play on the participant card in case autoplay was blocked by the browser.
- If calls only fail on some networks, add TURN server credentials. STUN-only setups often fail across stricter Wi-Fi or mobile networks because browsers cannot always reach each other directly.
- If the UI says
Connection failed. Add a TURN server for different networks., your signaling server is reachable, but the browsers could not form a direct media path. Configure TURN on Render or inclient/config.js. - If the UI says
Check the TURN relay settings., TURN is present but the relay details are wrong or unreachable. Double-check the TURN host, port, username, password, and whether you needturns:instead ofturn:.