AI-powered real-time collaboration platform — think Notion + Slack + ChatGPT in one MERN-stack app.
Teams can edit documents together in real time with live cursors, chat in channels and DMs, share files, and ask an AI assistant for help — all under one workspace with role-based access.
- What It Does
- Tech Stack
- Quick Start
- Environment Variables
- Project Structure
- Architecture Overview
- API Reference
- Socket Events
- Data Models
- How to Test Multi-User Features
- Roadmap
| Module | Features |
|---|---|
| 🔐 Auth | Email/password registration & login · bcrypt hashing · JWT tokens · protected routes |
| 🏢 Workspaces | Create workspaces · invite by email · 4-tier roles (Owner / Admin / Member / Viewer) · member management |
| 📝 Live Editor | Tiptap rich-text · real-time collaborative editing (Yjs CRDT) · live cursors with user color & name · auto-save every 3s · version history snapshots |
| 💬 Chat | Public/private channels · 1:1 DMs · message history with infinite scroll · emoji reactions · typing indicators · online presence · edit & delete |
| 📎 File Uploads | Drag-and-drop in chat · images, videos, audio, PDFs, docs · Cloudinary integration with local-disk fallback for dev |
| ✨ AI Assistant | Gemini-powered chat · conversation history · auto-generated titles · quick actions API (summarize / improve / fix grammar / generate / meeting notes / task list) · demo mode when no API key |
| 🛡️ Security | Helmet · CORS · rate limiting · Mongo sanitize · XSS clean · HPP · per-route auth/role middleware |
- React 18 (Vite)
- Tailwind CSS +
@tailwindcss/typography(for Tiptapprosecontent) - Zustand (state) with
persistmiddleware for auth - React Router v6
- Axios with auto-token interceptor
- Tiptap (ProseMirror-based editor) + Yjs + y-protocols + custom Socket.io provider for collab
- socket.io-client for chat/editor/presence
- react-hot-toast for notifications
- Node.js + Express 4
- MongoDB + Mongoose 8
- Socket.io 4 (JWT-authenticated)
- Yjs (server-side CRDT for live editing)
- jsonwebtoken + bcryptjs
- Zod for input validation
- multer for multipart uploads
- cloudinary SDK (with local-disk fallback)
- @google/generative-ai (Gemini)
- Security: helmet, cors, express-rate-limit, express-mongo-sanitize, xss-clean, hpp
- MongoDB Atlas (or local MongoDB)
- Cloudinary (optional — falls back to local
server/uploads/) - Google Gemini API (optional — falls back to demo replies)
- Node.js 18+
- MongoDB running locally OR a free MongoDB Atlas cluster
- (Optional) Gemini API key from aistudio.google.com
- (Optional) Cloudinary account from cloudinary.com
cd server
cp .env.example .env # fill in MONGO_URI and JWT_SECRET (rest optional)
npm install
npm run devServer runs at http://localhost:5000. Visit /api/health to verify it's up.
cd client
cp .env.example .env # defaults work for local dev
npm install
npm run devApp runs at http://localhost:5173.
Windows PowerShell users: use
copy .env.example .envinstead ofcp.
# Required
NODE_ENV=development
PORT=5000
CLIENT_URL=http://localhost:5173
MONGO_URI=mongodb://127.0.0.1:27017/collabai
JWT_SECRET=replace-with-long-random-string-min-32-chars
JWT_EXPIRES_IN=7d
# Optional — without this, AI runs in demo mode (mock replies)
GEMINI_API_KEY=
# Optional — without these, files save to ./uploads/ and serve at /uploads
CLOUDINARY_CLOUD_NAME=
CLOUDINARY_API_KEY=
CLOUDINARY_API_SECRET=VITE_API_URL=http://localhost:5000/api
VITE_SOCKET_URL=http://localhost:5000collabai/
├── README.md
├── .gitignore
│
├── client/ # React + Vite frontend
│ ├── index.html
│ ├── vite.config.js
│ ├── tailwind.config.js
│ ├── postcss.config.js
│ ├── package.json
│ └── src/
│ ├── api/ # Axios clients per resource
│ │ ├── axios.js # base client + 401 interceptor
│ │ ├── auth.js
│ │ ├── workspaces.js
│ │ ├── documents.js
│ │ ├── chat.js
│ │ ├── ai.js
│ │ └── uploads.js
│ │
│ ├── components/
│ │ ├── ProtectedRoute.jsx
│ │ ├── ui/ # Button, Input, Spinner
│ │ ├── editor/ # CollabEditor, EditorToolbar
│ │ └── chat/ # ChannelSidebar, MessageList,
│ │ # MessageItem, MessageInput
│ │
│ ├── hooks/useAuth.js # auth hydration + socket bootstrap
│ │
│ ├── layouts/
│ │ ├── AuthLayout.jsx # split-screen login/register
│ │ └── DashboardLayout.jsx # sidebar nav after login
│ │
│ ├── lib/
│ │ ├── socket.js # socket.io-client singleton
│ │ └── yjsSocketProvider.js # Yjs <-> Socket.io provider
│ │
│ ├── pages/
│ │ ├── Landing.jsx
│ │ ├── auth/{Login,Register}.jsx
│ │ ├── Dashboard.jsx
│ │ ├── Workspaces.jsx
│ │ ├── WorkspaceDetail.jsx # docs + members + invite
│ │ ├── Documents.jsx # all docs across workspaces
│ │ ├── DocumentEditor.jsx # full-screen editor page
│ │ ├── Chat.jsx
│ │ ├── AIAssistant.jsx
│ │ ├── ComingSoon.jsx
│ │ └── NotFound.jsx
│ │
│ ├── store/ # Zustand stores
│ │ ├── authStore.js
│ │ ├── workspaceStore.js
│ │ ├── chatStore.js
│ │ └── aiStore.js
│ │
│ ├── App.jsx # all routes
│ ├── main.jsx # bootstrap + Toaster
│ └── index.css # Tailwind + component classes
│
└── server/ # Express backend
├── package.json
└── src/
├── config/
│ ├── env.js # validated env loader
│ ├── db.js
│ ├── gemini.js
│ └── cloudinary.js
│
├── models/ # Mongoose schemas
│ ├── User.model.js
│ ├── Workspace.model.js
│ ├── Document.model.js
│ ├── DocumentVersion.model.js
│ ├── Channel.model.js
│ ├── Message.model.js
│ ├── AIConversation.model.js
│ └── Upload.model.js
│
├── controllers/ # Route handlers
│ ├── auth.controller.js
│ ├── user.controller.js
│ ├── workspace.controller.js
│ ├── document.controller.js
│ ├── channel.controller.js
│ ├── message.controller.js
│ ├── ai.controller.js
│ └── upload.controller.js
│
├── routes/ # Express routers
│ ├── auth.routes.js
│ ├── user.routes.js
│ ├── workspace.routes.js
│ ├── document.routes.js
│ ├── chat.routes.js
│ ├── ai.routes.js
│ └── upload.routes.js
│
├── middleware/
│ ├── auth.middleware.js
│ ├── workspace.middleware.js
│ ├── validate.middleware.js
│ ├── upload.middleware.js
│ └── error.middleware.js
│
├── validators/ # Zod schemas
│ ├── auth.validator.js
│ ├── workspace.validator.js
│ ├── document.validator.js
│ ├── chat.validator.js
│ └── ai.validator.js
│
├── services/ # Business logic
│ ├── ai.service.js
│ └── upload.service.js
│
├── sockets/ # Socket.io handlers
│ ├── index.js # auth + connect/disconnect dispatch
│ ├── auth.js # JWT middleware for sockets
│ ├── presence.js # online/offline tracking
│ ├── document.socket.js # Yjs collab rooms
│ └── chat.socket.js # send/typing/react/edit/delete
│
├── utils/
│ ├── ApiError.js
│ ├── ApiResponse.js
│ ├── asyncHandler.js
│ └── slug.js
│
├── app.js # Express app + middleware
└── server.js # HTTP + Socket.io bootstrap
POST /api/auth/registerand/loginissue a single JWT (default 7-day lifetime)- Frontend persists token in
localStoragevia Zustandpersistmiddleware - Axios interceptor attaches
Authorization: Bearer <token>to every request - 401 responses trigger automatic logout
- Socket.io connections authenticate via
auth: { token }handshake field; the socket attaches the user to every event handler
The hardest part of the platform. Pipeline:
- User opens
/documents/:id - Frontend instantiates a
Y.Docand a customSocketIOProvider - Provider emits
doc:join→ server validates workspace membership and returns the document's binary state (Y.encodeStateAsUpdate, base64-encoded) - Tiptap binds to the same
Y.Docvia@tiptap/extension-collaboration - Every local edit emits a Yjs update over Socket.io → server applies to its in-memory
Y.Docand broadcasts to the room - Other clients merge the update — CRDT guarantees zero conflicts even with concurrent edits
- Awareness protocol (
y-protocols/awareness) syncs cursor positions and user metadata for live cursors - Server debounces saves: 3 seconds after the last edit, the binary state + plaintext + a version snapshot get persisted to MongoDB
- When the last user leaves a room, the in-memory doc is evicted (after a final flush)
- Same Socket.io connection used for the editor
- Each channel has a room
chan:<channelId>; joining requires workspace membership and (for private/DM) explicit channel membership - Messages persist to MongoDB synchronously, then broadcast to the room
- Presence: in-memory
Map<userId, Set<socketId>>— broadcastspresence:online/presence:offlineto workspace rooms when a user's last/first socket connects - Typing: client emits debounced (2 s)
chat:typing, broadcast to the channel room - Reactions: stored inline on
Messageas[{ emoji, users[] }]; toggle pattern - Pagination: cursor-based on
_id(ObjectIds are time-sortable), 50 per page, infinite scroll up
- Each user has their own conversations (
AIConversationmodel holds a thread of{role, content}messages) POST /api/ai/conversations/:id/messagesappends the user turn, sends the last 20 turns as context to Gemini 1.5 Flash, persists the reply- First-exchange titles are auto-generated by a second Gemini call
- No API key?
ai.service.jsreturns a mock reply explaining how to enable Gemini — every UI flow remains testable - Rate limit: 30 requests/min/user
POST /api/uploadsacceptsmultipart/form-datawith fieldfile- Multer parses into memory (10 MB cap, MIME whitelist)
- If Cloudinary creds are present →
upload_stream→ returnssecure_url - Otherwise → write to
server/uploads/<random>.<ext>→ returns/<host>/uploads/<filename>(served byexpress.static) - Chat messages attach the upload metadata;
MessageItemrenders image/video/audio inline and other types as download cards
All routes are prefixed /api. All non-auth routes require Authorization: Bearer <token>.
| Method | Path | Body | Returns |
|---|---|---|---|
| POST | /register |
{name, email, password} |
{user, token} |
| POST | /login |
{email, password} |
{user, token} |
| GET | /me |
— | {user} |
| Method | Path | Notes |
|---|---|---|
| GET | / |
List workspaces the current user belongs to |
| POST | / |
Create a workspace; creator becomes Owner |
| GET | /:id |
Get one (must be member) |
| PATCH | /:id |
Update (admin+) |
| DELETE | /:id |
Delete (owner only) |
| POST | /:id/members |
Invite by email (admin+) |
| PATCH | /:id/members/:userId |
Change role (admin+) |
| DELETE | /:id/members/:userId |
Remove member (admin+) |
| Method | Path | Notes |
|---|---|---|
| GET | /workspaces/:workspaceId/documents |
List docs in workspace |
| POST | /workspaces/:workspaceId/documents |
Create (member+) |
| GET | /documents/:id |
Get metadata + role |
| PATCH | /documents/:id |
Rename (member+) |
| DELETE | /documents/:id |
Delete (admin+) |
| GET | /documents/:id/versions |
Version history (50 most recent) |
| Method | Path | Notes |
|---|---|---|
| GET | /workspaces/:workspaceId/channels |
{channels, dms} |
| POST | /workspaces/:workspaceId/channels |
Create channel |
| GET | /channels/:id |
Get with members populated |
| PATCH | /channels/:id |
Rename |
| DELETE | /channels/:id |
Delete (creator or workspace admin) |
| POST | /channels/:id/join |
Self-join a public channel |
| POST | /dm |
{workspaceId, userId} → get or create 1:1 DM |
| GET | /channels/:channelId/messages?before=&limit= |
Paginated history |
| POST | /channels/:channelId/messages |
REST send (also via socket) |
| PATCH | /messages/:id |
Edit own message |
| DELETE | /messages/:id |
Soft-delete (own message or workspace admin) |
| POST | /messages/:id/reactions |
Toggle reaction {emoji} |
| Method | Path | Notes |
|---|---|---|
| GET | /status |
{enabled, provider, model} |
| GET | /conversations |
List with previews |
| POST | /conversations |
Create new conversation |
| GET | /conversations/:id |
Get with full message history |
| PATCH | /conversations/:id |
Rename |
| DELETE | /conversations/:id |
Delete |
| POST | /conversations/:id/messages |
Send turn, get AI reply |
| POST | /quick |
{action, text} — quick action without conversation |
Quick action keys: summarize, improve, fix-grammar, generate, meeting-notes, task-list.
| Method | Path | Body | Notes |
|---|---|---|---|
| POST | / |
multipart/form-data with field file |
Single file, max 10 MB |
| POST | /multiple |
field files[] |
Up to 5 files |
All sockets must authenticate via the handshake: io(URL, { auth: { token } }).
| Direction | Event | Payload |
|---|---|---|
| C → S | doc:join |
{documentId} → ack {ok, state (base64), role} |
| C → S | doc:update |
{documentId, update (base64)} |
| C → S | awareness:update |
{documentId, update (base64)} |
| C → S | doc:leave |
{documentId} |
| S → C | doc:update |
{update, from} |
| S → C | awareness:update |
{update, from} |
| S → C | presence:join / presence:leave |
{socketId, user?} |
| Direction | Event | Payload |
|---|---|---|
| C → S | chat:join / chat:leave |
{channelId} |
| C → S | chat:send |
{channelId, content, replyTo?, attachments?} |
| C → S | chat:typing |
{channelId, isTyping} |
| C → S | chat:react |
{messageId, emoji} |
| C → S | chat:edit |
{messageId, content} |
| C → S | chat:delete |
{messageId} |
| S → C | chat:message |
{message} |
| S → C | chat:edit |
{message} |
| S → C | chat:delete |
{messageId, channelId} |
| S → C | chat:reaction |
{messageId, reactions} |
| S → C | chat:typing |
{channelId, userId, user, isTyping} |
| Direction | Event | Payload |
|---|---|---|
| C → S | presence:list |
{workspaceId} → ack {ok, online: [userId]} |
| S → C | presence:online |
{userId} |
| S → C | presence:offline |
{userId} |
name, email (unique, lowercased), password (bcrypt, select: false), avatar, isEmailVerified, workspaceIds[], lastLoginAt, timestamps.
name, slug (auto-generated, unique), description, icon, owner, members: [{user, role, joinedAt}] where role ∈ {owner, admin, member, viewer}, timestamps.
title, workspace, createdBy, lastEditedBy, yState (Buffer — Yjs binary state), plainText (extracted for search/preview), timestamps. Companion DocumentVersion for snapshots.
workspace, type ∈ {channel, dm}, name, description, isPrivate, members[], createdBy, lastMessageAt, timestamps. DMs are type: 'dm' with exactly 2 members.
channel, sender, content, attachments: [{url, name, mimeType, size, kind, publicId, storage}], replyTo, reactions: [{emoji, users[]}], editedAt, deletedAt (soft delete), timestamps.
user, title, messages: [{role, content, createdAt}] where role ∈ {user, assistant}, timestamps.
uploadedBy, url, publicId, name, mimeType, size, kind ∈ {image, video, audio, pdf, document, other}, storage ∈ {cloudinary, local}, timestamps.
The real value of this app is collaboration. To test it end-to-end:
- Open two browsers (e.g. Chrome + Chrome Incognito, or Chrome + Edge)
- Register two accounts:
alice@test.comandbob@test.com - As Alice → create a workspace → open it → Members section → invite
bob@test.comas Member - As Alice → create a document → copy the URL
- Paste the URL into Bob's browser
- You should now see:
- Both avatars in the editor header
- Each user's cursor in the doc with their name and a unique color
- Live updates as either user types
- Auto-save indicator after pauses
- In Chat: both join
#general→ type → see typing dots, reactions, edit, delete, file attachments live - Start a DM from either side → online dot turns green/grey as the other user connects/disconnects
| Status | Phase | Scope |
|---|---|---|
| ✅ | 1 | Auth + Workspaces + Roles |
| ✅ | 2 | Real-time editor (Yjs + Tiptap + custom Socket.io provider) |
| ✅ | 3 | Real-time chat (channels, DMs, presence, typing, reactions, edit/delete) |
| ✅ | 4 | AI assistant (Gemini) + file uploads (Cloudinary/local) |
| ⏳ | 5 | Notifications (mentions, invites) · Analytics dashboard · Redis caching · Docker · CI/CD |
- Email verification & forgot-password flows (no SMTP wired)
- Google OAuth (placeholder env vars present)
- Version history UI (snapshots are stored, restore UI not built)
- Reply-to UI (model + render supports it, no "Reply" button yet)
- AI quick actions inside the editor (backend ready, toolbar buttons not added)
- Search across docs and chat (no full-text index yet)
- Streaming AI responses (currently non-streaming with a "thinking" spinner)
| Script | What it does |
|---|---|
npm run dev |
Run with nodemon (auto-restart on file change) |
npm start |
Run with plain node (for production) |
| Script | What it does |
|---|---|
npm run dev |
Vite dev server (HMR) |
npm run build |
Production build → dist/ |
npm run preview |
Serve the built output locally |
- Frontend → Vercel (or Netlify): set
VITE_API_URLandVITE_SOCKET_URLto your production backend URL - Backend → Render (or Railway/Fly.io): set all
server/.envvars, point to MongoDB Atlas, ensure WebSocket support is enabled - Database → MongoDB Atlas free tier is enough to start
- Files → Cloudinary is recommended in production (local disk doesn't persist on Render's ephemeral filesystem)
MIT — use it, learn from it, ship it.
Built with the MERN stack — React + Express + MongoDB + Node.js.