A collaborative project management platform — organize projects, track work on a kanban board, and collaborate with your team in one place. Built with the MERN stack (MongoDB, Express, React/Next.js, Node.js).
- Features
- Tech stack
- Quick start (Docker)
- Manual setup
- Environment variables
- Architecture overview
- ER diagram
- API documentation
- Folder structure
- Deployment guide
- Testing
- Known limitations
- Auth — register, login, email verification, forgot/reset password, rotating refresh tokens
- Workspaces — invite teammates by email, role-based permissions (owner/admin/member)
- Projects — organize work, markdown notes, archive, live stats dashboard
- Kanban board — drag-and-drop tasks across Todo / In Progress / Review / Done
- Tasks — priority, due dates, labels, checklists, multiple assignees, file attachments, threaded comments with @mentions
- Notifications — task assignments, comments, and mentions, delivered to a notifications inbox (backend emits these live over Socket.io — see the architecture note below)
- Global search — full-text search across projects and tasks
- Dark mode, responsive layout throughout
| Layer | Technology |
|---|---|
| Frontend | Next.js 14 (App Router), TypeScript, Tailwind CSS, React Query, React Hook Form, Zod, Framer Motion, @dnd-kit |
| Backend | Node.js, Express, MongoDB, Mongoose |
| Auth | JWT access + rotating refresh tokens, email verification, password reset |
| Realtime | Socket.io (backend) |
| Storage | Cloudinary (avatars, task attachments) |
| Resend (transactional email API) | |
| Docs | Swagger / OpenAPI |
| Testing | Jest, Supertest, mongodb-memory-server |
| Tooling | Docker Compose, GitHub Actions CI, ESLint, Prettier |
The fastest way to get the whole stack running.
Requirements: Docker Desktop (or Docker Engine + Compose).
git clone <your-fork-url> devflow
cd devflow
docker compose up --buildThen open:
- Frontend: http://localhost:3000
- Backend health check: http://localhost:5000/health
- API docs (Swagger UI): http://localhost:5000/api/v1/docs
The compose file spins up MongoDB, the backend, and the frontend together, with
sensible defaults baked in — no .env file is required to get it running. Without a
RESEND_API_KEY configured, email sending falls back to console logging, so
registration and invite flows still work end-to-end; you'll just read the
verification/reset links from the backend container logs instead of an inbox:
docker compose logs -f backendTo load realistic demo data (a workspace, a project, and several tasks) once the containers are up:
docker compose exec backend npm run seedThis creates three demo accounts, all using the password password123:
| Role | |
|---|---|
ada@devflow-demo.test |
Owner |
grace@devflow-demo.test |
Admin |
alan@devflow-demo.test |
Member |
To stop everything: docker compose down (add -v to also delete the MongoDB volume).
Requirements: Node.js 20+, MongoDB 7+ (local install or MongoDB Atlas).
cd backend
cp .env.example .env
# edit .env - at minimum set MONGO_URI, JWT_ACCESS_SECRET, JWT_REFRESH_SECRET
npm install
npm run devThe API starts on http://localhost:5000. Generate strong secrets for the two JWT
values, for example:
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"Optionally load demo data: npm run seed.
In a second terminal:
cd frontend
cp .env.example .env.local
# NEXT_PUBLIC_API_URL defaults to http://localhost:5000/api/v1, which matches the
# backend above - only change it if your backend runs somewhere else
npm install
npm run devThe app starts on http://localhost:3000.
cd backend
npm testUses mongodb-memory-server, so no separate database is needed — it downloads and runs
a temporary MongoDB instance automatically (requires normal internet access, since it
fetches the MongoDB binary the first time you run it).
| Variable | Description | Required |
|---|---|---|
MONGO_URI |
MongoDB connection string | Yes |
JWT_ACCESS_SECRET / JWT_REFRESH_SECRET |
Long random strings signing the two token types | Yes |
CLIENT_URL |
Frontend origin, used for CORS and links in emails | Yes (defaults to http://localhost:3000) |
PORT |
Backend port | No (default 5000) |
RESEND_API_KEY |
Transactional email (verification, reset, invites) | No — without it, emails are logged to the console instead of sent¹ |
CLOUDINARY_CLOUD_NAME / CLOUDINARY_API_KEY / CLOUDINARY_API_SECRET |
File uploads | No — required only for avatar/attachment uploads to work |
RATE_LIMIT_WINDOW_MS / RATE_LIMIT_MAX |
API rate limiting | No |
¹ Variable name shown as RESEND_API_KEY per Resend's standard convention — confirm
this matches whatever your email.service.js actually reads before deploying.
See backend/.env.example for the full list with defaults.
| Variable | Description |
|---|---|
NEXT_PUBLIC_API_URL |
Base URL of the backend API, e.g. http://localhost:5000/api/v1 |
flowchart LR
FE["Next.js<br/>(App Router)"]
API["Express REST API<br/>(Node.js)"]
WS["Socket.io"]
DB[("MongoDB")]
CDN["Cloudinary"]
MAIL["Resend<br/>(email)"]
FE -- "HTTPS / REST + JWT" --> API
API -- Mongoose --> DB
API --> CDN
API --> MAIL
API -. events .-> WS
WS -. "planned: live push" .-> FE
Backend follows a layered architecture: routes (HTTP + validation) →
controllers (thin, request/response only) → services (business logic) → models
(Mongoose schemas). Every response follows a consistent shape ({ success, message, data } / { success: false, message, errors? }), produced by a single global error
handler. See CONTINUE.md for the full set of conventions established while building
this out (auth strategy, permission middleware chain, kanban position algorithm,
notification triggers, etc.) if you want the detailed reasoning behind specific
decisions.
Frontend uses the Next.js App Router with route groups: (auth) for the
unauthenticated split-screen auth pages, (app) for the authenticated shell (sidebar +
topbar), guarded by a client-side redirect that also preserves the original destination
(so an invite-email link survives a login redirect). Server state lives in React Query;
the only client state kept outside it is the in-memory access token and the kanban
board's local drag state.
Note on real-time notifications: the backend fully implements Socket.io
(JWT-authenticated connections, private per-user rooms, events emitted on task
assignment/comments/mentions — see backend/src/config/socket.js), but the frontend
doesn't yet open a socket connection to consume it; the notifications inbox currently
works via a 30-second React Query poll instead, which is a fine fallback but means
notifications aren't instant. Wiring up socket.io-client (already a frontend
dependency) in a small hook that invalidates the notifications query on incoming events
would close this gap — see Known limitations.
erDiagram
USER ||--o{ REFRESH_TOKEN : has
USER ||--o{ WORKSPACE_MEMBER : "is member via"
WORKSPACE ||--o{ WORKSPACE_MEMBER : has
WORKSPACE ||--o{ INVITATION : has
WORKSPACE ||--o{ PROJECT : contains
PROJECT ||--o{ TASK : contains
TASK ||--o{ COMMENT : has
TASK ||--o{ ATTACHMENT : has
TASK }o--o{ USER : "assigned to"
USER ||--o{ NOTIFICATION : receives
USER {
ObjectId _id
string name
string email
string password
string avatarUrl
boolean isEmailVerified
}
WORKSPACE {
ObjectId _id
string name
string slug
ObjectId owner
}
PROJECT {
ObjectId _id
ObjectId workspace
string name
string key
string status
number taskCounter
}
TASK {
ObjectId _id
ObjectId project
number number
string title
string status
string priority
number position
date dueDate
}
COMMENT {
ObjectId _id
ObjectId task
ObjectId author
string content
}
ATTACHMENT {
ObjectId _id
ObjectId task
string url
string publicId
}
NOTIFICATION {
ObjectId _id
ObjectId recipient
string type
boolean isRead
}
Full interactive API docs (Swagger UI) are served by the running backend at
/api/v1/docs (e.g. http://localhost:5000/api/v1/docs). Every route is documented
with its request/response shape directly from the source (@openapi JSDoc blocks above
each route definition in backend/src/routes/).
devflow/
├── backend/
│ ├── src/
│ │ ├── app.js, server.js # Express app + entry point
│ │ ├── config/ # env, db, cloudinary, socket, swagger
│ │ ├── models/ # Mongoose schemas
│ │ ├── services/ # business logic
│ │ ├── controllers/ # HTTP request/response handlers
│ │ ├── routes/ # route definitions + API docs
│ │ ├── middleware/ # auth, permissions, validation, errors
│ │ ├── validators/ # express-validator chains
│ │ ├── utils/ # ApiError, ApiResponse, helpers
│ │ └── scripts/seed.js # demo data
│ └── tests/ # Jest unit + integration tests
├── frontend/
│ └── src/
│ ├── app/ # Next.js App Router pages
│ │ ├── (auth)/ # login, register, password reset, etc.
│ │ └── (app)/ # authenticated shell: dashboard, workspaces,
│ │ # projects (kanban board), settings
│ ├── components/ # ui/ (primitives), kanban/, app/ (shell), etc.
│ ├── hooks/ # React Query hooks per resource
│ ├── lib/ # api clients, validation schemas, utils
│ └── context/ # auth context
├── docker-compose.yml
└── CONTINUE.md # full build log / architectural decision record
- Push this repo to GitHub.
- Create a new Web Service, point it at the
backend/directory as the root. - Build command:
npm install. Start command:npm start. - Set the environment variables from the table above (use a MongoDB Atlas connection
string for
MONGO_URIin production). - Set
CLIENT_URLto your deployed frontend's URL once you have it (step below).
- Create a free cluster at mongodb.com/atlas.
- Add a database user and allow network access from your backend host (or
0.0.0.0/0for simplicity while testing). - Copy the connection string into
MONGO_URI.
- Import the repo into vercel.com, set the root directory to
frontend/. - Set
NEXT_PUBLIC_API_URLto your deployed backend's API base URL (e.g.https://your-backend.onrender.com/api/v1). - Deploy. Then go back to your backend's environment variables and set
CLIENT_URLto this Vercel URL, so CORS and email links point to the right place.
Create a free account at cloudinary.com, grab your cloud name,
API key, and API secret from the dashboard, and set the three CLOUDINARY_* backend
environment variables.
Create a free account at resend.com, generate an API key, and set
RESEND_API_KEY in the backend environment. Verify a sending domain in the Resend
dashboard before going to production, or use their shared test domain during
development.
cd backend
npm test # run once
npm run test:watchTests use mongodb-memory-server for a real (in-memory, ephemeral) MongoDB instance, so
they exercise actual database behavior rather than mocks — integration tests cover the
full auth flow, workspace permissions, kanban task ordering, and file uploads (with
Cloudinary itself mocked, since that's a genuine external network dependency).
A few trade-offs were made deliberately to keep this a realistic, maintainable portfolio project rather than an over-engineered one — worth knowing about, and reasonable things to mention if this comes up in an interview:
- Deleting a workspace does not cascade-delete its projects/tasks/comments/attachments (only project deletion cascades to its tasks). A production system would handle this with a background cleanup job or a transaction.
- Comment "edit" support exists in the API and UI, but there's no edit history — an edited comment just shows an "(edited)" marker.
- The backend's Socket.io layer is fully built (see Architecture overview), but the frontend consumes notifications via polling rather than a live socket connection — a good next step, and a natural thing to discuss if asked "what would you improve next?"
MIT