A real-time collaborative Kanban workspace for developers, startups, and agile teams — built to feel like a commercial SaaS product, not a tutorial clone.
Live Demo · Features · Tech Stack · Getting Started · Deployment · API Reference
Most Kanban apps feel outdated — delayed updates, page refreshes, cluttered UI, poor collaboration. DevSync is built around the opposite premise: every interaction should feel instant, fluid, and alive. Multiple people editing the same board should feel like being in the same room — live cursors, live task updates, live presence — with zero manual refreshing, and animation and depth used deliberately (3D card tilt, spring transitions, glassmorphism) rather than as decoration.
It's a full-stack project meant to demonstrate production-grade engineering end to end: real-time WebSocket architecture, optimistic UI, drag-and-drop, authentication, file uploads, rate limiting, input sanitization, code-split bundles, and a deploy pipeline — not just CRUD screens.
⚠️ Replace this once deployed: https://your-app.vercel.app
| Frontend | https://dev-sync-pied.vercel.app |
| Backend API | https://devsync-czb3.onrender.com/api |
| Health check | https://devsync-czb3.onrender.com/api/health |
The backend is on Render's free tier, which spins down after inactivity — the first request after a while may take 30–60s to wake up.
|
Authentication
Workspaces & Boards
Columns & Tasks
Real-Time Collaboration
|
Comments & Activity
Notifications
Search
Profile & Settings
Dashboard
Polish
|
Frontend
| Framework | React 19 + TypeScript + Vite |
| Styling | Tailwind CSS v4 |
| Animation | Framer Motion |
| Routing | React Router v7 |
| Server state | TanStack Query |
| Client state | Zustand |
| Forms | React Hook Form + Zod |
| Drag & drop | DnD Kit |
| Real-time | Socket.io Client |
| Charts | Recharts |
| Testing | Vitest + Testing Library |
Backend
| Runtime | Node.js + Express 5 |
| Database | MongoDB + Mongoose |
| Real-time | Socket.io |
| Auth | JWT (httpOnly cookies) + bcrypt |
| File storage | Cloudinary (Multer memory storage → stream upload) |
| Validation | express-validator |
| Security | Helmet, express-rate-limit, hand-rolled NoSQL-injection sanitizer |
| Testing | Vitest + Supertest |
Every library here was picked deliberately, not by default — Zustand over Redux for the smaller footprint this app's scope needs, TanStack Query so server state is never duplicated into Zustand, DnD Kit over react-beautiful-dnd because it's actively maintained, Cloudinary so uploads never touch the app server's disk.
Add screenshots here after your first deploy — drop PNGs into
docs/assets/screenshots/and reference them like the row below. Suggested shots: Dashboard, Board view (mid-drag), Task modal, live cursors with two browser windows side by side, Login/Register.
flowchart LR
Browser["Browser<br/>React SPA"]
API["Express API<br/>REST"]
WS["Socket.io<br/>WebSocket server"]
DB[("MongoDB Atlas")]
CDN["Cloudinary<br/>media storage"]
Browser -- "HTTPS / REST" --> API
Browser <-- "WebSocket" --> WS
API --> DB
WS -. "relays events only<br/>REST is source of truth" .-> Browser
API -- "upload stream" --> CDN
Every mutation goes Component → Service → REST API → MongoDB first; the socket layer is a pure relay that broadcasts the confirmed change to everyone else in the room, so there's a single source of truth and no risk of the UI diverging from the database.
devsync/
├── backend/
│ └── src/
│ ├── config/ # db + cloudinary setup
│ ├── controllers/ # request handlers
│ ├── middleware/ # auth, upload, sanitize
│ ├── models/ # Mongoose schemas
│ ├── routes/ # Express routers
│ ├── services/ # business logic
│ ├── socket/ # Socket.io relay layer
│ ├── validators/ # express-validator chains
│ └── server.js
├── frontend/
│ └── src/
│ ├── components/ # shared UI (Button, Modal, layout shells...)
│ ├── hooks/ # useBoardData, useBoardSocket, useAuth...
│ ├── pages/ # route-level feature folders
│ ├── services/ # API + socket client layer
│ ├── store/ # Zustand (auth, theme, ui — UI state only)
│ ├── types/ # shared TS types
│ └── utils/
├── docs/assets/ # README images (logo, screenshots)
├── backend/render.yaml # Render deploy config
└── frontend/vercel.json # Vercel SPA rewrite config
Server state (boards, tasks, comments...) lives in TanStack Query; only UI/session state (auth,
theme, sidebar) lives in Zustand. Components never call the API directly — everything routes through
a services/ layer, and no socket event is ever emitted directly from a component.
- Node.js 20+
- A MongoDB instance (local, or a free Atlas cluster)
- A free Cloudinary account (for avatar/attachment uploads)
git clone https://github.com/<your-username>/devsync.git
cd devsynccd backend
cp .env.example .env # fill in the values — see table below
npm install
npm run dev # http://localhost:5000cd frontend
cp .env.example .env # defaults already point at localhost:5000
npm install
npm run dev # http://localhost:5173Open http://localhost:5173, register an account, and you're in.
backend/.env
| Variable | Description |
|---|---|
NODE_ENV |
development locally, production when deployed |
PORT |
Port the API listens on (Render sets this itself) |
CLIENT_URL |
Frontend origin, for CORS + cookie config |
MONGO_URI |
MongoDB connection string |
JWT_SECRET |
Long random string for signing auth tokens |
JWT_EXPIRES_IN |
Token lifetime, e.g. 7d |
CLOUDINARY_CLOUD_NAME |
From your Cloudinary dashboard |
CLOUDINARY_API_KEY |
From your Cloudinary dashboard |
CLOUDINARY_API_SECRET |
From your Cloudinary dashboard |
frontend/.env
| Variable | Description |
|---|---|
VITE_API_BASE_URL |
Backend REST base, e.g. http://localhost:5000/api |
VITE_SOCKET_URL |
Backend socket origin, e.g. http://localhost:5000 |
Backend
| Command | What it does |
|---|---|
npm run dev |
Start with nodemon (auto-restart) |
npm start |
Start for production |
npm test |
Run the Vitest + Supertest suite |
Frontend
| Command | What it does |
|---|---|
npm run dev |
Start the Vite dev server |
npm run build |
Type-check (tsc -b) + production build |
npm run preview |
Serve the production build locally |
npm test |
Run the Vitest + Testing Library suite |
npm run lint |
Lint with oxlint |
cd backend && npm test # health check, auth validation, auth-guard paths
cd frontend && npm test # utils, shared UI componentsCurrent coverage is smoke-level (validated end to end by hand during development, including a full browser pass with real drag-and-drop, sockets, and modal interactions) — not exhaustive integration or E2E coverage. See "Known Limitations" below.
DevSync deploys as two independent services plus two managed cloud dependencies. Both deploy configs already live in the repo — connect them and fill in secrets.
- Create a free cluster at mongodb.com/atlas
- Create a database user + password
- Network Access → allow
0.0.0.0/0(or Render's static outbound IPs for tighter security) - Copy the
mongodb+srv://...connection string → this becomesMONGO_URI
- Create a free account at cloudinary.com
- Copy
Cloud name,API Key,API Secretfrom the dashboard
- New Web Service → connect this repo → Render auto-detects
backend/render.yaml - Fill in the env vars it leaves blank:
CLIENT_URL,MONGO_URI,CLOUDINARY_CLOUD_NAME,CLOUDINARY_API_KEY,CLOUDINARY_API_SECRET(JWT_SECRETauto-generates) - Deploy — health check is
/api/health
- New Project → connect this repo → set root directory to
frontend - Vercel auto-detects
frontend/vercel.json(handles the SPA rewrite so client-side routes survive a refresh) - Set env vars:
VITE_API_BASE_URL=https://<your-render-service>.onrender.com/apiandVITE_SOCKET_URL=https://<your-render-service>.onrender.com - Deploy
Go back to Render and set CLIENT_URL to your live Vercel URL, then redeploy the backend once.
Cross-domain auth cookies are already handled — generateToken.js switches to
sameSite: 'none', secure: true automatically once NODE_ENV=production.
All routes are prefixed with /api. Full request/response contracts are in each *.controller.js.
Auth — /auth
| Method | Path | Description |
|---|---|---|
| POST | /register |
Create account |
| POST | /login |
Log in |
| POST | /logout |
Clear session |
| GET | /profile |
Current user |
| PUT | /profile |
Update name / avatar |
| PUT | /password |
Change password |
| POST | /forgot-password |
Request reset link |
| POST | /reset-password |
Complete reset |
Workspaces — /workspaces
| Method | Path | Description |
|---|---|---|
| POST | / |
Create workspace |
| GET | / |
List my workspaces |
| GET | /:id |
Get workspace |
| PUT | /:id |
Update workspace |
| DELETE | /:id |
Delete workspace |
| POST | /:id/invite |
Invite member |
| DELETE | /:id/members/:memberId |
Remove member |
| POST | /:id/leave |
Leave workspace |
Boards — /boards
| Method | Path | Description |
|---|---|---|
| POST | / |
Create board |
| GET | /workspace/:workspaceId |
List boards in workspace |
| GET | /:id |
Get board |
| PUT | /:id |
Update board |
| DELETE | /:id |
Delete board (cascades columns/tasks) |
| POST | /:id/favorite |
Toggle favorite |
| POST | /:id/labels |
Add label |
| DELETE | /:id/labels/:labelId |
Remove label |
Columns — /columns
| Method | Path | Description |
|---|---|---|
| POST | / |
Create column |
| POST | /reorder |
Reorder columns |
| GET | /board/:boardId |
List columns |
| PUT | /:id |
Rename column |
| DELETE | /:id |
Delete column |
Tasks — /tasks
| Method | Path | Description |
|---|---|---|
| POST | / |
Create task |
| GET | /board/:boardId |
List tasks on board |
| GET | /:id |
Get task |
| PUT | /:id |
Update task |
| PATCH | /:id/move |
Move between columns / reorder |
| DELETE | /:id |
Delete task |
| POST | /:id/checklist |
Add checklist item |
| PATCH | /:id/checklist/:itemId |
Toggle checklist item |
| DELETE | /:id/checklist/:itemId |
Remove checklist item |
| POST | /:id/attachments |
Upload attachment |
| DELETE | /:id/attachments/:attachmentId |
Remove attachment |
Comments, Notifications, Activity, Search
| Method | Path | Description |
|---|---|---|
| POST | /comments/task/:taskId |
Add comment |
| GET | /comments/task/:taskId |
List comments |
| PUT | /comments/:id |
Edit comment |
| DELETE | /comments/:id |
Delete comment |
| GET | /notifications |
List notifications |
| PUT | /notifications/read-all |
Mark all read |
| PUT | /notifications/:id/read |
Mark one read |
| GET | /activity/board/:boardId |
Board activity log |
| GET | /search?q= |
Global search |
| Client emits | Server broadcasts |
|---|---|
join-board, leave-board |
user-online, user-offline |
task-created, task-updated, task-deleted, task-moved |
task-updated |
column-created, column-updated |
column-updated |
cursor-move, typing |
presence-update |
join-user-room |
new-notification |
Sockets are a relay layer only — every mutation persists via REST first, then broadcasts. See
backend/src/socket/index.js.
Deliberately out of scope for this pass:
- No transactional email provider — password reset link is logged server-side / returned in dev
- No Google OAuth
- No per-type notification preferences (all types fire; no backend schema for opt-out yet)
- Smoke-level automated tests only — no live-DB integration tests or E2E suite yet
- No virtualized lists — untested at very large (100+ tasks/column) scale
This started as a solo/portfolio build, but issues and PRs are welcome. A few conventions worth
knowing before opening a PR: components stay under ~200 lines, server data never goes in Zustand,
API calls always go through services/, and Framer Motion animations stay under 400ms.
No license file yet — all rights reserved by default until one is added. If you want this open for reuse, MIT is the usual choice for a project like this.
Built with ❤️ to feel like a product, not a project.