Hey everyone!
I'm a 20-year-old self-taught developer from Thailand preparing to break into the industry as a junior full-stack/backend dev.
I recently built an open-source group video conferencing and real-time chat app called Chatn. When I started, I thought I could just follow a basic WebRTC tutorial. But as soon as I tried scaling beyond 2 users on localhost, things broke down fast.
Here are the main technical hurdles I ran into and how I solved them:
- The P2P WebRTC Bottleneck (Switching to LiveKit SFU)
My first prototype used a standard WebRTC P2P mesh. It worked fine between 2 browser tabs. But with 4–5 participants, my laptop fan went crazy and video froze.
In P2P mesh, connections scale as O(N²). For 5 participants, every client has to encode and upload their video stream to 4 other peers (O(N) upload bandwidth). On typical home Wi-Fi with low upload bandwidth, the uplink gets choked immediately.
The Fix: I switched to an SFU (Selective Forwarding Unit) using LiveKit. With an SFU, each client encodes and uploads only 1 stream to the server (O(1) upload). The server then forwards the video packets to the other peers. Client CPU usage dropped, and video stayed smooth even with multiple users.
- Separating Video from Persistent Chat (Laravel Reverb)
LiveKit has built-in data channels for text messaging, but WebRTC data channels are ephemeral. If someone joins the meeting 10 minutes late, they won't see any previous messages.
I wanted real message persistence, authorization policies, and chat history. So I separated concerns:
LiveKit: Dedicated strictly to heavy UDP video/audio packet routing. Laravel Reverb (WebSockets) + MySQL: Handles real-time chat, authentication, DB transactions, and presence events. - The "Shifting Window" Bug (Cursor Pagination)
For chat history, I initially used traditional offset pagination (LIMIT 20 OFFSET 20).
In an active room, if someone is scrolling up through old messages while 5 new messages are being sent, the offset shifts. The user ends up seeing duplicate messages or skipping messages entirely.
The Fix: Switched to keyset / cursor pagination (WHERE id < :last_seen_id ORDER BY id DESC LIMIT 20). Because the query is anchored to a specific message ID, new incoming messages at the top never alter the pagination window. It also does an instant primary key B-Tree index seek instead of scanning and discarding thousands of rows in MySQL.
- Testing WebRTC on Mobile over LAN (Docker + Caddy + nip.io)
When I tried testing video calls between my laptop and physical phone on my Wi-Fi (http://192.168.1.x), mobile browsers immediately blocked getUserMedia(). Browsers require a Secure Context (HTTPS) for camera and microphone permissions.
The Fix:
Containerized the entire stack with Docker Compose. Put Caddy in front as a reverse proxy. Used nip.io wildcard DNS (https://192-168-1-50.nip.io). Caddy automatically generates and trusts local SSL certificates, allowing my phone to access the app over HTTPS on the local network with zero certificate friction. Tech Stack
Backend API: PHP 8.2, Laravel 11, Laravel Reverb (WebSockets), MySQL Media Server: LiveKit (SFU) Frontend: Next.js 14 (App Router), TypeScript, Tailwind CSS, TanStack Query, Zustand DevOps: Docker, Docker Compose, Caddy Reverse Proxy GitHub Repositories (Open Source)
Orchestration: https://github.com/nayhtetkhant/chatn-main Backend API: https://github.com/nayhtetkhant/chatn-api Frontend: https://github.com/nayhtetkhant/chatn-web Portfolio: https://nayhtetkhant.com/ I would love to get your thoughts, constructive feedback, or any potential architectural bottlenecks you spot in the repos!