This is my final project for Computer Science A.
Chatle is a full-stack real-time chat app where users add each other as friends, then exchange direct or group messages with images, replies, edits, typing indicators, seen receipts, and one-to-one video calls — plus AI bot users you can chat with like any other contact.
I used Claude and CodeRabbit to review parts of the code and get feedback on bugs, structure, and improvements.
Accounts
- Signup, login, and logout with JWT stored in an httpOnly cookie
- Email verification with a 6-digit code, capped at 5 emails per account with a 30-second resend cooldown
- Unverified accounts are removed automatically by a MongoDB TTL index
- Profile updates, including an avatar uploaded through Cloudinary
People
- Friend requests: send, accept, decline, cancel, and unfriend
- Block a user, which hides them from your contacts, chats, and search and stops their realtime messages reaching you. Blocking is one-directional, so the blocked user is not affected
- Per-viewer nicknames for other users, visible only to the person who set them
- Pin chats to the top of your own chat list
Messaging
- Direct messages and group conversations
- Text, images, or both, with replies to an earlier message
- Edit, delete, and pin messages
- Typing indicators, online-user presence, and seen receipts
- Groups: create, rename, add or remove participants, and leave
Calls
- One-to-one video calls with ring, answer, decline, and end, using Socket.IO for signalling and Stream for the call itself
AI bots
- Bot users you chat with like any other contact, backed by Groq or Gemini
Interface
- Three themes and an English/Khmer language switcher
- Keystroke, notification, and ringtone sound effects
- Responsive React interface
| Layer | Choice |
|---|---|
| Frontend | React, Vite, Tailwind CSS, daisyUI |
| Frontend state | Zustand |
| Routing | React Router |
| Localization | English/Khmer dictionaries in src/i18n |
| Backend | Node.js, Express |
| Database | MongoDB, Mongoose |
| Real-time updates | Socket.IO |
| Authentication | JWT in httpOnly cookies, bcrypt |
| Resend | |
| Image uploads | Cloudinary |
| AI providers | Groq, Gemini |
| Video calls | Stream |
| Protection | Arcjet |
flowchart LR
subgraph Frontend["React + Vite Frontend"]
UI["Pages and Components"]
Store["Zustand Stores"]
end
subgraph Backend["Express Backend"]
Routes["REST API Routes"]
Middleware["Route Middleware"]
Controllers["Controllers"]
Socket["Socket.IO Server"]
SocketAuth["Socket Auth Middleware"]
AI["AI Provider Layer"]
end
Mongo[("MongoDB")]
Cloudinary[("Cloudinary")]
Resend[["Resend"]]
Stream[["Stream"]]
Arcjet{{"Arcjet"}}
Groq[["Groq"]]
Gemini[["Gemini"]]
UI --> Store
Store -- "HTTP requests" --> Routes
Store -- "Socket.IO connection" --> Socket
Routes --> Middleware
Middleware -- "rate limit / bot checks" --> Arcjet
Middleware --> Controllers
Controllers --> Mongo
Controllers --> Cloudinary
Controllers --> Resend
Controllers --> Stream
Controllers --> AI
AI --> Groq
AI --> Gemini
Socket --> SocketAuth
SocketAuth --> Mongo
Socket -- "room lookup" --> Mongo
Controllers -- "emit events" --> Socket
Socket -- "live updates" --> Store
The frontend uses HTTP requests for normal app actions, such as logging in, loading chats, sending messages, uploading images, and creating groups. Protected routes check the JWT cookie, and most app routes also pass through Arcjet. Socket.IO is used for live updates, including new messages, online users, typing status, seen receipts, group updates, and call events.
- The user writes a message in the React app.
- The frontend sends the message to the backend with an HTTP request.
- The backend checks authentication and Arcjet protection.
- The backend saves the message in MongoDB.
- The backend emits a Socket.IO event to the other user or group room.
- Connected clients receive the event and update the chat UI.
Direct messages use:
POST /api/messages/send/:id
Group messages use:
POST /api/conversations/:id/messages
AI bots are stored as normal users in MongoDB. A bot user has isBot: true,
plus a botProvider and botModel.
When a user sends a text message to a bot:
- The backend saves the user's message.
- The backend loads recent messages from that conversation.
backend/lib/ai/index.jsselects the bot's provider.- The provider calls Groq or Gemini.
- The backend saves the bot reply.
- The backend sends the reply to the user with Socket.IO.
The app supports groq and gemini provider keys. The bot's provider is chosen
from the bot user record. The code does not automatically switch providers if
one fails.
The Socket.IO server lives in backend/lib/socket.js and shares the same
Express app as the REST API, so both run on one port. Connections are
authenticated by reading the same jwt cookie the REST API uses, in
socket.auth.middleware.js.
Two delivery paths exist:
- Direct messages and presence use an in-memory
userSocketMapofuserId -> socketId. Because this map lives in the server process, the app assumes a single backend instance. - Group messages use Socket.IO rooms named after the conversation id. On
connect, the server joins the socket to every group the user belongs to, and
the
joinConversationevent covers groups created while the user is already online.
Events the server emits: getOnlineUsers, newMessage, newGroupMessage,
messagesSeen, userTyping, userStoppedTyping, addedToGroup,
removedFromGroup, groupUpdated, incomingCall, callAnswered,
callDeclined, callEnded, callFailed, and botReplyFailed.
Video calls use Socket.IO only for signalling. The call id is the two user ids
sorted and joined with a hyphen, and the media session itself runs through
Stream using a token from GET /api/stream/token.
chatle/
├── backend/
│ ├── lib/
│ │ ├── ai/ # AI provider selection and prompts
│ │ ├── arcjet.js # Arcjet setup
│ │ ├── cloudinary.js # Cloudinary upload setup
│ │ ├── db.js # MongoDB connection
│ │ ├── env.js # Environment variables
│ │ ├── resend.js # Email client
│ │ ├── socket.js # Socket.IO server
│ │ └── stream.js # Stream video client
│ ├── scripts/ # Bot setup and maintenance scripts
│ └── src/
│ ├── controllers/ # Request handlers
│ ├── emails/ # Email templates and send helpers
│ ├── middleware/ # Auth, socket auth, and Arcjet middleware
│ ├── models/ # Mongoose models
│ ├── routes/ # API routes
│ └── server.js # Express server entry point
├── frontend/
│ ├── public/ # Static files and demo assets
│ └── src/
│ ├── components/ # React components
│ ├── i18n/ # English and Khmer text
│ ├── lib/ # Axios setup
│ ├── pages/ # App pages
│ └── store/ # Zustand stores
├── package.json
└── README.md
- Node.js 20 or newer
- npm
- MongoDB 6 or newer, either local or a MongoDB Atlas connection string
The app calls several external services, so signing up for these is part of local setup. Each one is free to start with:
- Resend — required. Signup will not complete without a working verification email.
- Cloudinary — required for avatar and image uploads.
- Arcjet — required, because every app route passes through it.
- Stream — needed for video calls only.
- Groq or Gemini — needed for AI bot replies only.
See Environment Variables for the keys each one provides.
git clone https://github.com/SrunLyheang/chat.git chatle
cd chatleCreate backend/.env before starting the server, using the
Environment Variables section below. The backend
will not connect to MongoDB or sign auth cookies without it.
Start the backend:
cd backend
npm install
npm run devStart the frontend in another terminal:
cd frontend
npm install
npm run devThe backend runs on:
http://localhost:3000
The frontend runs on:
http://localhost:5173
In development the frontend talks to http://localhost:3000/api directly, set
in frontend/src/lib/axios.js, so both servers need to be running.
AI bots are ordinary user records, so one has to be created before you can chat with it:
cd backend
node scripts/seedBot.js
node scripts/seedBot.js groq-bot@yourapp.local "Groq Bot" groq llama-3.3-70b-versatileThe first command creates a Gemini-backed bot, and the second shows the argument order for creating a Groq-backed one.
Create backend/.env.
PORT=3000
NODE_ENV=development
CLIENT_URL=http://localhost:5173
MONGO_URI=your_mongodb_connection_string
JWT_SECRET=your_jwt_secret
RESEND_API_KEY=your_resend_api_key
EMAIL_FROM=your_verified_sender_email
EMAIL_FROM_NAME=Chatle
CLOUDINARY_CLOUD_NAME=your_cloudinary_cloud_name
CLOUDINARY_API_KEY=your_cloudinary_api_key
CLOUDINARY_API_SECRET=your_cloudinary_api_secret
ARCJET_KEY=your_arcjet_key
ARCJET_ENV=development
STREAM_API_KEY=your_stream_api_key
STREAM_API_SECRET=your_stream_api_secret
GROQ_API_KEY=your_groq_api_key
GEMINI_API_KEY=your_gemini_api_key
BOT_USER_ID=optional_seeded_bot_user_id| Variable | Purpose |
|---|---|
PORT |
Backend port. Defaults to 3000. |
NODE_ENV |
App environment. Use production for production builds. |
CLIENT_URL |
Frontend URL allowed by CORS and Socket.IO. |
MONGO_URI |
MongoDB connection string. |
JWT_SECRET |
Secret used to sign JWT auth cookies. |
RESEND_API_KEY |
Resend API key for email verification. |
EMAIL_FROM |
Sender email address for Resend. |
EMAIL_FROM_NAME |
Sender display name. |
CLOUDINARY_CLOUD_NAME |
Cloudinary cloud name. |
CLOUDINARY_API_KEY |
Cloudinary API key. |
CLOUDINARY_API_SECRET |
Cloudinary API secret. |
ARCJET_KEY |
Arcjet key for rate limiting and bot protection. |
ARCJET_ENV |
Arcjet environment name. |
STREAM_API_KEY |
Stream API key for video calls. |
STREAM_API_SECRET |
Stream API secret for video calls. |
GROQ_API_KEY |
Groq API key for AI bot replies. |
GEMINI_API_KEY |
Gemini API key for AI bot replies. |
BOT_USER_ID |
Optional ID used by bot scripts. |
Do not commit real secrets. Use environment variables or a secret manager in production.
This app is not currently deployed anywhere, and the repository contains no hosting configuration. What follows is how the code expects to be run in production, not a description of a live environment.
The project is built to deploy as a single service. When NODE_ENV is
production, backend/src/server.js serves the built frontend from
frontend/dist and falls back to index.html for any unmatched route, so the
REST API, the Socket.IO server, and the static frontend all run on one port.
The scripts in the root package.json follow that shape:
npm run build # installs backend and frontend deps, then builds the frontend
npm start # runs the backend, which also serves frontend/distFor a production run, set every variable from Environment Variables, with:
NODE_ENV=production, which makes the auth cookiesecureand switches the Arcjet rate limiter fromDRY_RUNtoLIVECLIENT_URLset to the deployed origin, since it controls both the CORS allowlist and the Socket.IO origin check
Two constraints are worth knowing before choosing a host:
- Single instance only. Online-user presence and direct-message routing are held in an in-memory map in the server process. Running more than one instance would split that state, so scaling out would need a shared adapter such as Redis first.
- Cloudflare is assumed.
backend/lib/arcjet.jsis configured withproxies: [cloudflare()], meaning Arcjet reads the real client IP from Cloudflare's headers. Deploying without Cloudflare in front would make that client IP untrustworthy, so this setting should be changed to match whatever proxy actually sits in front of the app.
- Protected REST routes use JWT authentication.
- Socket.IO connections are authenticated with the JWT cookie.
- Arcjet is used on most app API routes for rate limiting and bot protection.
- Images are uploaded through the backend before being stored in Cloudinary.
- Production deployments should use trusted CORS origins and secure secrets.
- Socket.IO does not connect: Check
CLIENT_URL, cookie settings, andwithCredentials. - Emails do not arrive: Check Resend API key, sender setup, and recipient rules.
- Images do not upload: Check all Cloudinary environment variables.
- AI bot does not reply: Check the bot user record, provider key, model name, and API key.
- Video calls do not work: Check
STREAM_API_KEYandSTREAM_API_SECRET.