My friend group and I had grown tired of Discord's quirks, and as we say in Italy chi fa da sé fa per tre. So I built a full-stack Discord clone: text/voice servers, DMs, P2P video and screen sharing, roles, presence — a single project to tackle the typical problems of a real-time multi-user app.
The backend is NestJS + Prisma + PostgreSQL with Socket.IO for chat, presence and WebRTC signaling; the frontend is Next.js 15 + React 19 + Zustand. Voice and video go peer-to-peer over a WebRTC mesh, auth is JWT access token + httpOnly refresh cookie with rotation, and permissions are a bitmask checked in O(1).
The result is a working Discord-like app with optimistic UI, cursor-paginated messages, an LRU client cache, magic-byte upload validation, and reconnect-safe sockets. The only open limitation is the in-memory StateService: scaling past one instance requires moving it to Redis.
Run with npm install && npm run dev (client :3000, server :3001).
- Servers and channels: server creation, text and voice channels, ordering, icons.
- Messages: send, edit, delete, reply threading, emoji reactions, attachments with preview.
- DMs: persistent 1-to-1 conversations with the same UX as channels.
- Voice and video: join/leave voice channel, mute, speaking indicator, screen sharing, mesh P2P.
- Presence:
ONLINE / IDLE / DND / OFFLINEstates, unread badges for both channels and DMs. - Permissions: bitmask roles (
SEND=1,MANAGE_MSG=2,MANAGE_CH=4,MANAGE_ROLES=8,KICK=16,BAN=32,MANAGE_SERVER=64,ADMIN=128); the server owner bypasses every check. - Invites: reusable codes with usage/expiry limits, per-server ban list.
- Auth: JWT access token (header) + refresh token in httpOnly cookie, rotation on refresh.
- Presence/voice cleanup on disconnect — handled in
PresenceGateway.handleDisconnect. This is the spot where it is easiest to leave "ghost" users online. - Reconnect without losing rooms —
activeRoomstracked on the socket layer avoids having to reload state on every network glitch. - Bitmask permissions — replace N joins: a single
IntonRole, checked with&in O(1). - Client-side LRU cache — the naive "remember every message I've ever seen" cache grows forever; 15 channels is the sweet spot found between UX and RAM.
- WebRTC mesh — trivial to implement but dies past ~6 users: an accepted trade-off because the use case is "small groups".
- Optimistic UI with
clientIdreconciliation — the server re-emits the originalclientId, so the client can swap the optimistic message in O(1) without flicker. - Magic-byte upload validation — the client
Content-Typeis not trustworthy;file-typereads the actual leading bytes of the file. - Non-scalable in-memory state — known and documented limitation: going beyond a single instance requires moving
StateServiceonto Redis.