This project demonstrates a production-ready WhatsApp-style chat interface with advanced tagging capabilities:
- ✔ Real-time bidirectional messaging (Socket.io WebSockets)
- ✔ Dynamic tagging with
@mentionsand#hashtags - ✔ Debounced live autocomplete from backend API
- ✔ Visual tag highlighting with transparent input overlay
- ✔ Smart backspace (deletes entire tag atomically)
- ✔ Token-based message architecture for rich rendering
- ✔ Redux state management with optimistic updates
- ✔ Room-based chat with shareable links
- ✔ Modular, scalable frontend + backend architecture
| Technology | Purpose | Version |
|---|---|---|
| Next.js | React framework with App Router | 14+ |
| Redux Toolkit | Centralized state management | Latest |
| Socket.io-client | Real-time WebSocket communication | 4.x |
| React Query | Server state, caching, mutations | 5.x |
| TypeScript | Type safety across components | 5.x |
| Tailwind CSS | Utility-first styling | 3.x |
| @uidotdev/usehooks | Custom React hooks (debouncing) | Latest |
| Technology | Purpose |
|---|---|
| Node.js + Express | RESTful API server |
| Socket.io | WebSocket server for real-time events |
| CORS | Cross-origin resource sharing |
| Prisma/TypeORM | Database ORM (suggested) |
| **PostgreSQL | Persistent data storage |
Trigger Detection
- Monitors cursor position on every keystroke
- Detects
@or#characters followed by alphanumeric text - Extracts search keyword dynamically
Autocomplete Flow
User types "@joh"
↓
Extract keyword: "joh"
↓
Debounce 250ms (prevents API spam)
↓
GET /api/suggestions?q=joh
↓
Backend returns: ["john", "johnny", "johnson"]
↓
Display dropdown below input
↓
User selects with Enter/Click
↓
Replace "@joh" → "@john " (with space)
↓
Add "@john" to mentions[] array
↓
Visual highlighting applied
Smart Backspace
- Detects if cursor is after a complete tag (e.g.,
@john|) - Deletes entire tag atomically instead of character-by-character
- Removes tag from mentions array
- Repositions cursor correctly
Transparent Input Overlay Technique
<div className="relative">
{/* Colored overlay (non-interactive) */}
<div className="absolute inset-0 pointer-events-none">
{value.split(/(\s+)/).map(part =>
mentions.includes(part)
? <span className="bg-yellow-300">{part}</span>
: <span>{part}</span>
)}
</div>
{/* Transparent input (user types here) */}
<input
value={value}
className="relative z-10 bg-transparent"
style={{color: "transparent"}}
/>
</div>Why This Approach?
- ✅ Native input behavior (cursor, selection, mobile keyboard)
- ✅ No contenteditable complexity or cursor jumping
- ✅ Custom styling without fighting browser defaults
- ✅ Works perfectly on iOS/Android
What Are Tokens?
Messages are stored as structured arrays instead of plain strings:
// Plain text storage (limited)
"Hello @john check #urgent report"
// Token-based storage (flexible)
[
{type: "text", value: "Hello "},
{type: "tag", label: "john", trigger: "@"},
{type: "text", value: "check "},
{type: "tag", label: "urgent", trigger: "#"},
{type: "text", value: "report "}
]Benefits
- 🎨 Rich rendering with custom styles per token type
- 🔍 Efficient search/filtering by tags
- 🔒 XSS protection (no HTML injection)
- 🚀 Future extensibility (links, emojis, files)
UUID-Based User Identification
// Anonymous user system (no signup required)
let userId = localStorage.getItem("userId");
if (!userId) {
userId = crypto.randomUUID(); // "a3f5b2c1-..."
localStorage.setItem("userId", userId);
}Room Join Flow
- User opens
/room/[roomId] - Check/generate user ID
- POST
/api/rooms/joinwith{roomId, userId} - Fetch message history: GET
/api/rooms/[id]/messages - Connect to Socket.io and emit
join-room - Setup message listener
- Render chat UI with history
Shareable Room Links
// Copy room link button
const shareRoom = () => {
const url = window.location.href; // https://app.com/room/abc-123
navigator.clipboard.writeText(url);
alert("Room link copied! Share with others.");
};- Node.js 18+ installed
- npm or yarn package manager
- (Optional) PostgreSQL/MongoDB for persistence
cd backend
npm install
PORT=5050
NODE_ENV=development
DATABASE_URL=postgresql://user:pass@localhost:5432/chatdb
EOF
# Run development server
npm run dev
# Server starts on http://localhost:5050Backend API Endpoints
POST /api/messages # Save new message
GET /api/messages/:id # Get single message
GET /api/rooms/:id/messages # Get room history
POST /api/rooms/join # Join room
GET /api/suggestions?q=keyword # Autocomplete suggestions
Socket.io Events
// Client → Server
socket.emit("join-room", roomId);
socket.emit("room-message", {roomId, message});
// Server → Client
socket.on("room-message", (message) => {
// Handle incoming message
});cd frontend
bun install
bun dev
# Run development server
npm run dev
# App starts on http://localhost:3000 ┌──────────────────────────┐
│ FRONTEND │
│ (Next.js) │
├──────────────────────────┤
│ • Chat UI (React) │
│ • TagInput Component │
│ • Highlight Overlay │
│ • Redux Store │
│ • Socket.io Client │
│ • React Query Cache │
└─────────────┬────────────┘
│
REST (axios) WebSocket
/api/messages (Socket.io)
/api/suggestions
│
▼
┌────────────────────────────────────────────────────────────────┐
│ BACKEND │
│ Node.js + Express.js │
├────────────────────────────┬───────────────────────────────────┤
│ REST API Layer │ WebSocket Server (Socket.io) │
│ • GET /suggestions?q= │ • Broadcast room messages │
│ • POST /messages │ • Handle join-room events │
│ • GET /rooms/:id/messages │ • Emit room-message events │
│ • POST /rooms/join │ • Room-based namespacing │
└───────────────┬────────────┴───────────────┬───────────────────┘
│ │
│ │
▼ ▼
┌────────────────────────┐ ┌──────────────────────────┐
│ Autocomplete Service │ │ Message Broadcast Queue │
│ • Search suggestions │ │ • Room isolation │
│ • Filter by keyword │ │ • Event broadcasting │
│ • Return sorted list │ │ • Connection management │
└─────────┬──────────────┘ └─────────────┬────────────┘
│ │
▼ ▼
┌────────────────────────────┐ ┌─────────────────────────┐
│ DATABASE LAYER │ │ DATABASE LAYER │
│ • suggestions table │ │ • messages table │
│ - id, label, type │ │ - id, content (JSON) │
│ • users table │ │ - roomId, senderId │
│ - id, name │ │ - createdAt │
└────────────────────────────┘ │ • rooms table │
│ - id, participants[] │
└─────────────────────────┘
User Frontend Backend
│ │ │
│ Types "@akha" │ │
│──────────────────────────►│ │
│ │ 1. handleChange() │
│ │ - getValue() │
│ │ - getCursor() │
│ │ - getCurrentWord() │
│ │ → "@akha" │
│ │ │
│ │ 2. Detect trigger │
│ │ trigger = '@' │
│ │ searchWord = 'akha' │
│ │ setShowList(true) │
│ │ │
│ │ 3. useDebounce(250ms) │
│ │ [Wait for typing pause] │
│ │ │
│ │ 4. useSuggestionQuery() │
│ │ GET /suggestions?q=akha │
│ │───────────────────────────────►│
│ │ │ 5. Query DB
│ │ │ SELECT * FROM
│ │ │ suggestions
│ │ │ WHERE label
│ │ │ ILIKE 'akha%'
│ │ │
│ │ ◄──────────────────────────────│ 6. Return
│ │ [{label: "Akhand"}, │ ["Akhand",
│ │ {label: "Akhar"}] │ "Akhar"]
│ Dropdown renders │ │
│ ┌──────────────┐ │ │
│ │ @Akhand │ ←hover │ │
│ │ @Akhar │ │ │
│ └──────────────┘ │ │
│ │ │
│ Press ↓ ↓ (ArrowDown) │ │
│──────────────────────────►│ 7. handleKeyDown() │
│ │ activeIndex++ │
│ │ Highlight next item │
│ │ │
│ Press Enter │ │
│──────────────────────────►│ 8. replaceWord() │
│ │ - getWordBoundaries() │
│ │ {start: 0, end: 5} │
│ │ - Build: "@Akhand" │
│ │ - Replace in value │
│ │ - setMentions([...]) │
│ │ - setSelectionRange() │
│ │ │
│ Visual highlight │ 9. Overlay re-renders │
│ "@Akhand" in yellow │ <span class="bg-yellow"> │
│◄──────────────────────────│ @Akhand │
│ │ </span> │
User A (Browser 1) Server (Node.js) User B (Browser 2)
│ │ │
│ 1. Type "Hello @john" │ │
│ Press Enter │ │
├─────────────────────────►│ │
│ │ │
│ 2. Tokenize message │ │
│ [{type:"text", │ │
│ value:"Hello "}, │ │
│ {type:"tag", │ │
│ label:"john"}] │ │
├─────────────────────────►│ │
│ │ │
│ 3. POST /api/messages │ │
│ {tokens, senderId, │ │
│ roomId} │ │
├─────────────────────────►│ 4. Save to database │
│ │ INSERT INTO messages │
│ │ RETURNING id, ... │
│ │ │
│ ◄─────────────────────────│ 5. Return saved msg │
│ {id: "msg-123", │ with ID │
│ tokens: [...], │ │
│ createdAt: "..."} │ │
│ │ │
│ 6. Optimistic UI update │ │
│ (message already │ │
│ visible) │ │
│ │ │
│ 7. socket.emit() │ │
│ "room-message" │ │
├─────────────────────────►│ 8. Broadcast to room │
│ │ io.to(roomId).emit() │
│ │───────────────┬──────────│
│ │ │ │
│ │ └─────────►│ 9. Receive event
│ │ │ socket.on()
│ │ │
│ │ │ 10. Dispatch Redux
│ │ │ addRoomMessage()
│ │ │
│ │ │ 11. UI re-renders
│ │ │ New message
│ │ │ appears
│ Message in own bubble │ │◄───────────────
│ (right-aligned) │ │ Message in
│ │ │ other's bubble
│ │ │ (left-aligned)
| Optimization | Implementation | Impact |
|---|---|---|
| API Debouncing | useDebounce(250ms) |
80-90% fewer API calls |
| React Query Caching | 5-minute stale time | Instant repeat searches |
| Redux Selectors | Memoized with Reselect | Prevents unnecessary re-renders |
| Socket Event Cleanup | useEffect return cleanup | No memory leaks |
| Conditional Rendering | showList && list.length > 0 |
Reduced DOM nodes |
Virtual Scrolling for Large Message Lists
import { FixedSizeList } from "react-window";
<FixedSizeList
height={600}
itemCount={messages.length}
itemSize={80}
width="100%"
>
{({ index, style }) => (
<div style={style}>
<RoomMessageBubble msg={messages[index]} />
</div>
)}
</FixedSizeList>Message Pagination
// Load messages in batches
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ["room-messages", roomId],
queryFn: ({ pageParam = 0 }) =>
api.get(`/rooms/${roomId}/messages?offset=${pageParam}&limit=50`),
getNextPageParam: (lastPage) => lastPage.nextOffset,
});| Feature | Status | Notes |
|---|---|---|
| XSS Protection | ✅ Implemented | Token-based rendering prevents HTML injection |
| Authentication | ❌ Missing | Anyone can join any room |
| Rate Limiting | ❌ Missing | API can be spammed |
| CORS | origin: "*" allows any domain |
|
| Input Sanitization | ✅ Implicit | Tokenization sanitizes tags |
| SQL Injection | Use parameterized queries |
1. Add Authentication
// JWT-based auth
app.use("/api", authenticateJWT);
function authenticateJWT(req, res, next) {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}2. Rate Limiting
import rateLimit from "express-rate-limit";
const limiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 20, // 20 requests per minute
message: "Too many requests, please try again later."
});
app.use("/api/suggestions", limiter);3. Restrict CORS
app.use(cors({
origin: ["https://yourapp.com", "http://localhost:3000"],
credentials: true
}));1. Socket Connection Failed
Error: WebSocket connection failed
Solution:
- Check backend is running on correct port
- Verify
NEXT_PUBLIC_SOCKET_URLin frontend .env - Check CORS settings in backend
- Ensure firewall allows WebSocket connections
2. Dropdown Not Appearing
User types "@" but no suggestions show
Solution:
- Check API endpoint:
curl http://localhost:5050/api/suggestions?q=test - Verify debounce isn't too long (should be 250ms)
- Check React Query dev tools for query status
- Ensure
showListstate is being set totrue
3. Messages Not Syncing
User A sends message but User B doesn't receive
Solution:
- Verify both users joined same room ID
- Check socket event names match exactly (
"room-message") - Ensure
io.to(roomId).emit()is broadcasting correctly - Check browser console for socket errors
- Verify duplicate prevention isn't filtering messages incorrectly
4. Tags Not Highlighting
Tags send correctly but don't highlight visually
Solution:
- Verify tag is in
mentions[]array - Check
mentions.includes(part)logic in overlay - Ensure overlay has
pointer-events-noneCSS - Verify z-index stacking order (overlay above input)
Akhand Pratap
- Socket.io team for excellent real-time library
- Redux team for state management patterns
- Next.js team for amazing React framework
- Open-source community for inspiration
Built with ❤️ using Next.js, Redux, and Socket.io