Real-time chat app · 07 / 09
MERN Chat
One-to-one real-time chat with cookie-based JWT auth, live presence, image messages and 32 themes.
- Role
- Solo developer
- Year
- 2025
- Status
- Shipped
- Platform
- Web

Overview
The project.
A full-stack messaging app on the MERN stack. People sign up, see who is online and exchange text and image messages that arrive instantly, without a refresh.
It runs as a single Render service: one Express process serves the REST API, the Socket.IO server and the built React app from the same origin, which keeps cookie auth simple and hosting cheap.
The problem
A chat app has to feel instant and stay secure: real accounts, live delivery and presence, and media, without a heavy multi-service deployment.
My solution
REST for auth, history and sending; Socket.IO for push delivery and presence; httpOnly JWT cookies for sessions; Cloudinary for media; all served from one Express process.
What it does
Key features.
- 01
Secure accounts
Sign-up and login with validation, bcrypt-hashed passwords and a 7-day JWT in an httpOnly, SameSite-strict cookie. Sessions restore on reload.
- 02
Instant messaging
Each message is saved first, then pushed over Socket.IO only to the recipient's socket, so the open conversation updates live.
- 03
Online presence
A live list of connected users drives status dots and a “show online only” filter in the contacts sidebar.
- 04
Image messages
Attach an image with a preview before sending. Files go to Cloudinary and are stored as URLs.
- 05
Profiles
Avatar upload from the profile page, stored on Cloudinary.
- 06
32 themes
A DaisyUI theme picker with a live chat preview, remembered in localStorage.
Interface
Screens from the real app.


Under the hood
How it's built.
Architecture and the engineering decisions behind it.
01
One origin, one service
Express and Socket.IO share one HTTP server. In production Express also serves the Vite build with an SPA fallback, so the client calls a relative /api and the cookie flow stays same-origin.
02
REST and sockets, each for its job
Sending is a REST call that persists to MongoDB; Socket.IO only notifies. A userId-to-socket map routes newMessage events and broadcasts presence on connect and disconnect.
03
Small client stores
Zustand stores for auth and socket, chat and theme. The chat store attaches one listener per open conversation and removes it on switch.
04
Lean data model
Two Mongoose models, User and Message. A conversation's history is a single query on the sender and receiver pair.
Problems solved
Technical challenges.
SPA routing broke in production
An Express 5 upgrade broke the catch-all route that serves the React app. I moved back to Express 4 to keep the production SPA fallback reliable.
Cookies across dev and production
CORS origin and the API and socket base URLs switch by environment. Production is same-origin, so the SameSite-strict cookie works without third-party cookie problems.
No duplicate real-time handlers
The client subscribes to new messages only for the open conversation and cleans up on change, so a message never renders twice.
Development status · Shipped
What's next.
Everything above is built and working. These parts are planned and not implemented yet:
- Group conversations
- Unread counts and notifications for other chats
- Typing indicators and read receipts
- Paginated message history