Real-Time Chat App
Decoupled real-time messaging architecture using Next.js 15 and Socket.IO.
What I Built#
I built this project to learn how to implement real-time bi-directional messaging with WebSockets. It is a full-stack chat application where users can join chat rooms, send direct messages, see live typing indicators, and view who is currently online.
The project is split into two repositories:
- Frontend: Built with Next.js 15, React 19, and Tailwind CSS.
- Backend: A standalone Node.js and Express server using Socket.IO and MongoDB.
Why Decouple the Backend?#
Next.js is often deployed on serverless environments (like Vercel), where server instances shut down after executing a request. Because WebSockets require a persistent, long-lived TCP connection, running Socket.IO directly inside Next.js serverless functions is unreliable.
By building a separate Node.js/Express backend, the WebSocket server can run continuously on a persistent host while the Next.js frontend handles page rendering and UI state.
Architecture & Event Flow#
Next.js 15 Client (User A) Next.js 15 Client (User B)
│ │
│ WebSocket Connection (Socket.IO) │
▼ ▼
┌────────────────────────────────────────────────────────────┐
│ Express + Socket.IO Server │
│ ├── JWT Authentication & Socket Handshake │
│ ├── Room-Scoped Broadcasting ('send_msg', 'typing') │
│ └── Connection / Disconnect Listeners (Presence) │
└─────────────────────────────┬──────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────┐
│ MongoDB Database │
│ (User accounts, chat rooms, and message history) │
└────────────────────────────────────────────────────────────┘- 01Authentication & Handshake: Users sign in with JWT authentication. When the frontend loads, it initiates a Socket.IO connection and passes the auth token to establish an authenticated socket session.
- 02Room Management: When a user selects a contact or group, the client emits a
join_roomevent. The server registers the socket into a specific room ID so messages only broadcast to active participants of that conversation. - 03Live Typing Indicators: As a user types, debounced
typingandstop_typingevents broadcast to other members in the room, displaying a live "User is typing..." indicator without saving to the database. - 04Message Persistence: When a message is sent, the server broadcasts it to the room and commits it to MongoDB so past chat history loads when users return.
Challenges I Worked Through#
- Tracking Online / Offline Presence: If someone closes their browser tab without explicitly logging out, a naive system leaves them marked as online. By listening to Socket.IO's native
disconnectevent on the server, the backend immediately detects when a socket drops and broadcasts an updated online roster to all users. - Optimistic UI Updates: Waiting for the server to acknowledge a message before displaying it in the sender's chat bubble creates noticeable input lag. I added optimistic rendering on the client so sent messages appear immediately in the conversation thread with a small pending icon, which confirms once the server acknowledges receipt.
What I Learned#
- WebSockets need dedicated infrastructure: Keeping real-time socket connections on a separate server from stateless web frontends keeps both systems cleaner and easier to maintain.
- Optimistic UI makes chat feel native: Adding messages to the local state immediately makes the interface feel as responsive as desktop messaging apps.