Whisp is a simple, clean, real‑time chat app. Backend is Node/Express with MongoDB and JWT cookie auth; frontend is Vite + React with Socket.IO for live updates. The UI is styled to feel iMessage‑like and stays theme‑aware via DaisyUI.
Deep‑dive docs live in docs/:
docs/API.md— API reference with request/response examplesdocs/ARCHITECTURE.md— Folder structure and internal flowdocs/ENV.md— Environment variables and .env templates
- Backend: Node.js, Express 5, Mongoose (MongoDB), JWT, bcryptjs, Cloudinary, Socket.IO
- Frontend: Vite + React, Zustand, Tailwind + DaisyUI, Socket.IO Client
backend/
package.json # Backend scripts/deps
src/
index.js # Express entrypoint (serves frontend in production)
controllers/ # Route handlers
lib/ # DB connect, utils, cloudinary, socket server
middleware/ # Auth middleware (JWT cookie)
models/ # Mongoose models
routes/ # Express routers
frontend/
package.json # Frontend scripts/deps
src/ # React app
vite.config.js # Dev proxy /api -> backend:3000
- Backend
- Create
backend/.env(seedocs/ENV.md). Required:PORT,MONGODB_URL,JWT_SECRET, Cloudinary keys. - Start:
cd backend
npm install
npm run dev- Frontend
cd frontend
npm install
npm run devNotes
- Frontend uses a Vite proxy so any request to
/apigoes tohttp://localhost:3000. Axios is configured withwithCredentials: trueto send the JWT cookie. - Socket.IO in dev connects to
http://localhost:3000; in production it uses same‑origin by default.
When a user logs in, we open a Socket.IO connection and attach userId in the connection query. The server tracks online users and emits updates; when you send a message via REST, the server pushes it live to the receiver if they’re online.
sequenceDiagram
autonumber
participant UI as React UI
participant API as Express API (/api)
participant WS as Socket.IO Server
participant DB as MongoDB
UI->>API: POST /api/auth/login (email, password)
API->>UI: Set-Cookie jwt; HttpOnly; SameSite=Strict
UI->>WS: io.connect with userId
WS->>WS: map userId -> socket.id
WS->>UI: getOnlineUsers [userIds]
UI->>API: POST /api/message/send/:id { text, image? }
API->>DB: save message
API->>WS: emit newMessage to receiver
WS->>UI: receiver gets newMessage
Server keeps a simple userSocketMap and emits getOnlineUsers whenever someone connects/disconnects. The REST send endpoint persists the message, then emits newMessage to the receiver’s socket if online.
Socket events
- Server → Client:
getOnlineUsers(string[] of userIds),newMessage(Message) - Client → Server: connection lifecycle (Socket.IO handles it)
Base URL (dev): http://localhost:3000
Auth
- POST
/api/auth/signup— Create account (setsjwtcookie) - POST
/api/auth/login— Login (setsjwtcookie) - POST
/api/auth/logout— Logout (clears cookie) - GET
/api/auth/check— Return current user (protected) - PUT
/api/auth/update-profile— Update profile picture (protected)
Messages
- GET
/api/message/users— List other users (protected) - GET
/api/message/:id— Conversation with user:id(protected) - POST
/api/message/send/:id— Send message to user:id(protected)
Example (curl)
# Login and persist cookie
curl -i -c /tmp/cjar -b /tmp/cjar \
-X POST http://localhost:3000/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"secret123"}'
# Check current user using cookie
curl -i -b /tmp/cjar http://localhost:3000/api/auth/checkBackend (required)
PORT— e.g., 3000 (Render sets this automatically)MONGODB_URL— MongoDB connection stringJWT_SECRET— Secret for signing JWTCLOUDINARY_CLOUD_NAME,CLOUDINARY_API_KEY,CLOUDINARY_API_SECRET- Optional:
CLIENT_ORIGIN— allow CORS from a separate frontend origin (only needed if not serving frontend from backend)
Frontend (optional for custom hosting)
VITE_API_BASE— Override REST base (default:/api)VITE_SOCKET_URL— Override Socket.IO URL (default: dev →http://localhost:3000, prod → same‑origin)
Single Web Service (backend serves the built frontend)
- Create a Web Service from this repository (root)
- Set Build Command:
npm run build- Set Start Command:
npm start- Add env vars listed above (Backend section).
What the scripts do
- Root
package.json:build: installs backend + frontend deps, then builds the frontendstart: starts the backend (which servesfrontend/distin production)
After deploy
- Load your Render URL; the server will serve the SPA and expose REST under
/apiand Socket.IO under the same origin. - If you host the frontend separately, set
CLIENT_ORIGINon the backend and configureVITE_API_BASEandVITE_SOCKET_URLon the frontend host. For cross‑site cookies, switch toSameSite: "none"and keepsecure: true(ask us to adjust if you choose this route).
- Auth uses an HttpOnly JWT cookie named
jwtwithSameSite=Strict(secure in production). This is ideal when backend serves the frontend (same origin). - Protected routes require sending credentials; Axios is already configured with
withCredentials: true. - Express 5 is used; controllers use async/await with early returns.