Real-time chat application built with Next.js and Socket.IO.
- Next.js 16 (App Router, Server Components, Server Actions)
- React 19
- Tailwind CSS 4
- Radix UI (Dialog, Label, Slot)
- Socket.IO Client (real-time messaging)
- jose (JWT verification)
- bcryptjs (password hashing)
- Express 5 + Socket.IO 4 (WebSocket server)
- MongoDB + Mongoose 9 (database)
- Redis + ioredis (pub/sub, caching)
- Docker + Docker Compose (containerized deployment)
- Nginx (reverse proxy)
- BullMQ + sharp (background image processing)
apps/web Next.js frontend (port 3000)
apps/ws Express + Socket.IO server (port 4000)
packages/db Mongoose models + MongoDB connection
packages/redis Redis client, pub/sub, and cache utilities
nginx/ Nginx reverse proxy configuration
Monorepo managed with npm workspaces.
- Authentication via JWTs stored as HTTP-only cookies
- Client connects to Socket.IO for real-time messaging
- Messages are saved to MongoDB and published to Redis
- Redis pub/sub broadcasts messages to all connected clients in the room
- Contact lists and message history are cached in Redis
When a user sends images in a chat message, the WebSocket server handles them in two phases:
-
Immediate save & broadcast — The raw images are converted to base64 data URIs, saved to the message in MongoDB, and included in the real-time Socket.IO broadcast. This means all clients see the images instantly.
-
Async compression via BullMQ — A job is queued for each image on the
processMediaqueue. A BullMQ worker (apps/ws/workers/media.worker.ts) picks up each job, compresses the image using sharp (quality 70, preserving the original format), and replaces the uncompressed base64 in MongoDB with the compressed version using$seton the specific array index.
Client sends image
-> message.send handler converts to base64, saves to MongoDB, broadcasts via Redis pub/sub
-> addMediaJob() enqueues a job per image with { messageId, index, buffer, mimeType }
-> media.worker picks up the job, compresses with sharp, overwrites media[index] in MongoDB
The queue and worker both use a dedicated Redis connection with maxRetriesPerRequest: null (required by BullMQ for its blocking commands). Jobs retry up to 3 times with exponential backoff and are removed on completion or failure.
Key files:
apps/ws/queues/media.queue.ts— Queue definition andaddMediaJobhelperapps/ws/workers/media.worker.ts— Worker that compresses images with sharpapps/ws/events/message.send.ts— Event handler that saves, broadcasts, and enqueues
- Clone the repository:
git clone https://github.com/your-username/lychee-chat.git
cd lychee-chat- Create a
.envfile in the project root:
DATABASE_URI=mongodb://MONGO_ROOT_USER:MONGO_ROOT_PASSWORD@mongo:27017/MONGO_DB?authSource=admin
REDIS_URL=redis://default:REDIS_PASSWORD@redis:6379
JWT_SECRET_KEY=your-secret-key
MONGO_ROOT_USER=admin
MONGO_ROOT_PASSWORD=password
MONGO_DB=lychee-chat
REDIS_PASSWORD=password- Build and start all services:
docker compose up --build- Open http://localhost:8080 in your browser.
| Service | Description |
|---|---|
| web | Next.js frontend |
| ws | WebSocket server |
| mongo | MongoDB database |
| redis | Redis cache + pub/sub |
| nginx | Reverse proxy (exposed on port 8080) |
# Start in detached mode
docker compose up -d --build
# View logs
docker compose logs -f
# Stop all services
docker compose down
# Stop and remove volumes (resets database)
docker compose down -v# Install dependencies
npm install
# Start the Next.js dev server (port 3000)
cd apps/web && npm run dev
# Start the WebSocket server (port 4000)
cd apps/ws && node index.tsRequires MongoDB and Redis running locally or via cloud services. Configure connection strings in .env.