A multi-tenant project management platform β built from scratch to understand what actually goes into a system like Jira.
Projectify is a full-stack project management platform with a Kanban board, real-time collaboration, workspace-based multi-tenancy, and role-based permissions β similar to Jira or Linear, built from scratch.
The goal wasn't to replace existing tools. It was to make every engineering decision that goes into building one: where to store tokens, how to design the database schema, how to scope real-time events, how to handle the token refresh race condition that nobody writes about in tutorials.
Projectify follows a feature-based architecture where the React frontend communicates with the Express backend using REST APIs and Socket.io. Business logic is isolated inside services, Redis powers distributed rate limiting, and MongoDB stores all workspace, project, task, and notification data.
- JWT Authentication β HttpOnly cookies with dual-token system (15min access + 7day refresh). Token refresh race condition handled with a request queue
- Google OAuth β Passport.js with account linking for existing email/password accounts
- Role-Based Access Control β Per-workspace roles (owner / admin / member) enforced in middleware, not controllers
- Multi-tenant Workspaces β Complete data isolation between workspaces
- Project & Epic Management β Projects contain epics which group related tasks
- Kanban Task Board β Tasks with fractional position indexing for O(1) drag-and-drop reordering
- Real-time Updates β Socket.io with room-scoped events. Task updates reach only users viewing that board
- Analytics Dashboard β 7 parallel aggregation queries via Promise.all (~50ms vs ~350ms sequential)
- Search β Case-insensitive task search across workspace
- Notifications β Per-user event notifications with TTL auto-expiry after 30 days
- Tiered Rate Limiting β Three Redis-backed tiers: auth (10/15min), API (60/min), global (200/15min)
- Security Stack β Helmet, CSRF protection, NoSQL injection sanitization, HPP
- Structured Logging β Winston with JSON output in production, request ID tracing on every log line
- Kanban Board β Drag and drop with
@dnd-kit, optimistic updates with rollback on failure - Real-time sync β Socket.io room-based live updates without polling
- Command palette β Cmd+K search across projects and tasks
- Analytics charts β Recharts: area chart (completion trend), pie (status), bar (priority)
- Dark mode β System preference detection via next-themes
- Form validation β React Hook Form + Zod (same schemas as backend)
Projectify uses a dual-token authentication system with HttpOnly cookies, Google OAuth, automatic token refresh, and request queuing to eliminate refresh race conditions.
| Technology | Purpose | Why chosen |
|---|---|---|
| Node.js + Express | HTTP server | Async I/O fits this data-heavy workload |
| MongoDB + Mongoose | Database | Nested task data (checklists, arrays) maps naturally to documents |
| Redis (Upstash) | Rate limiting | Shared counter across server instances |
| Socket.io | Real-time | Room abstraction for scoped event delivery |
| JWT + Passport.js | Auth | Stateless sessions + Google OAuth |
| Zod | Validation | Schema-first, type coercion, same library on frontend |
| Winston | Logging | JSON structured output + custom http log level |
| Technology | Purpose |
|---|---|
| React + Vite | UI framework + build tool |
| TanStack Query v5 | Server state, caching, optimistic updates |
| Zustand | Client state (auth, UI) |
| TailwindCSS + Shadcn UI | Styling + accessible components |
| @dnd-kit | Drag and drop |
| Recharts | Analytics charts |
| Socket.io-client | Real-time connection |
task_manager/
βββ server/ # Backend
β βββ src/
β β βββ config/
β β β βββ db.js # MongoDB connection
β β β βββ redis.js # Redis/Upstash client
β β β βββ env.js # Env validation
β β β
β β βββ core/
β β β βββ errors/ # Custom error classes
β β β βββ logger/ # Winston setup
β β β βββ middleware/
β β β β βββ authenticate.js # JWT verification
β β β β βββ validate.js # Zod request validation
β β β β βββ rateLimiter.js # Tiered Redis rate limits
β β β β βββ csrfProtection.js
β β β β βββ validateObjectId.js
β β β βββ utils/
β β β βββ response.js # sendSuccess / sendCreated
β β β βββ socketEmitter.js # Centralized socket emissions
β β βββ features/
β β β βββ auth/ # Login, signup, Google OAuth
β β β βββ workspace/ # Workspace CRUD + members
β β β βββ projects/ # Projects + epics
β β β βββ tasks/ # Tasks + Kanban + search
β β β βββ analytics/ # Aggregation-based stats
β β β βββ notifications/ # User notifications
β β βββ seeders/
β β β βββ seed.js # Demo data seeder
β β βββ app.js # Express app + middleware chain
β β βββ server.js # HTTP server + Socket.io init
β βββ .env
β
βββ client/ # Frontend
βββ src/
βββ core/ # API client, stores, providers, router
βββ features/ # auth, workspace, projects, tasks, analytics
βββ shared/ # UI components, hooks, utils
- Node.js 18+
- MongoDB Atlas account (free M0 tier works)
- Upstash Redis account (free tier works)
- Google Cloud Console project (for OAuth)
git clone https://github.com/Siddhi561/Projectify
cd Projectifycd server
npm installCreate server/.env:
# Server
PORT=5000
NODE_ENV=development
# Database
MONGODB_URI=mongodb+srv://user:password@cluster.mongodb.net/projectify
# Redis (use rediss:// for Upstash TLS)
REDIS_URL=rediss://default:password@your-instance.upstash.io:6379
# JWT β generate with: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
JWT_ACCESS_SECRET=your_64_char_hex_here
JWT_REFRESH_SECRET=different_64_char_hex_here
# Frontend URL (for CORS)
CLIENT_URL=http://localhost:5173
# Google OAuth β from console.cloud.google.com
GOOGLE_CLIENT_ID=your_client_id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-your_secret
GOOGLE_CALLBACK_URL=http://localhost:5000/api/auth/google/callbackStart the server:
npm run devPopulate the database with realistic demo data for testing and development.
# Add demo data
npm run seed
# Clear existing data and reseed
npm run seed:clearThe seeder creates:
- π₯ 4 demo users
- π’ 2 workspaces
- π 4 projects
- π 8 epics
- β ~60 tasks
- π Sample notifications
Demo account
Email: demo@projectify.dev
Password: Demo@1234
cd ../client
npm install
# Install Shadcn UI components
npx shadcn-ui@latest init
npx shadcn-ui@latest add button input label textarea card dialog dropdown-menu \
select avatar badge separator progress skeleton scroll-area popover \
collapsible command tooltipCreate client/.env:
VITE_API_URL=http://localhost:5000Start the frontend:
npm run devOpen http://localhost:5173
Raw OpenAPI JSON:
http://localhost:5000/api/docs.json
| Method | Endpoint | Auth | Min Role |
|---|---|---|---|
| POST | /api/auth/signup | β | β |
| POST | /api/auth/login | β | β |
| GET | /api/auth/me | β | any |
| POST | /api/auth/logout | β | any |
| POST | /api/auth/refresh | β | β |
| GET | /api/workspaces | β | any |
| POST | /api/workspaces | β | any |
| GET | /api/workspaces/:id | β | member |
| PATCH | /api/workspaces/:id | β | admin |
| DELETE | /api/workspaces/:id | β | owner |
| POST | /api/workspaces/:id/members/invite | β | admin |
| PATCH | /api/workspaces/:id/members/:mId/role | β | admin |
| DELETE | /api/workspaces/:id/members/:mId | β | admin |
| GET | /api/workspaces/:id/projects | β | member |
| POST | /api/workspaces/:id/projects | β | member |
| GET | /api/workspaces/:id/projects/:pid | β | member |
| PATCH | /api/workspaces/:id/projects/:pid | β | member |
| DELETE | /api/workspaces/:id/projects/:pid | β | admin |
| GET | /api/workspaces/:id/projects/:pid/epics | β | member |
| POST | /api/workspaces/:id/projects/:pid/epics | β | member |
| PATCH | /api/workspaces/:id/projects/:pid/epics/:eid | β | member |
| DELETE | /api/workspaces/:id/projects/:pid/epics/:eid | β | admin |
| GET | /api/workspaces/:id/projects/:pid/tasks | β | member |
| GET | /api/workspaces/:id/projects/:pid/tasks/grouped | β | member |
| POST | /api/workspaces/:id/projects/:pid/tasks | β | member |
| GET | /api/workspaces/:id/tasks/:tid | β | member |
| PATCH | /api/workspaces/:id/tasks/:tid | β | member |
| DELETE | /api/workspaces/:id/tasks/:tid | β | creator/admin |
| POST | /api/workspaces/:id/tasks/reorder | β | member |
| GET | /api/workspaces/:id/tasks/search | β | member |
| GET | /api/workspaces/:id/stats | β | member |
| GET | /api/workspaces/:id/projects/:pid/stats | β | member |
| GET | /api/notifications | β | any |
| PATCH | /api/notifications/:nid/read | β | any |
| PATCH | /api/notifications/read-all | β | any |
localStorage can be read by any JavaScript on the page. HttpOnly cookies cannot be read by JavaScript at all β not your code, not an injected script. Token theft via XSS is eliminated at the storage layer.
The refresh token cookie has path: '/api/auth/refresh' β the browser only sends it on that one endpoint. Every other API call never carries the most sensitive token.
When multiple API calls get 401 simultaneously, a request queue ensures only one refresh fires. Others wait and retry after. Without this, concurrent 401s cause logout when refresh tokens rotate.
Task positions are floating point numbers. Inserting between positions 1000 and 2000 yields 1500 β only one document updated per drag. Integer positions would require updating every subsequent card.
Seven independent database queries run simultaneously instead of sequentially. Result: ~50ms instead of ~350ms for the analytics dashboard.
io.emit() sends to every connected user. Rooms send only to users viewing the relevant board. 500 users online, one task update β 10 receive it, 490 are unaffected.
Each feature owns its routes, controller, service, model, and schemas together. Services never touch req or res β they receive plain arguments and return plain data, making them independently testable and reusable.
| Layer | Protects Against |
|---|---|
| Helmet | XSS via CSP, clickjacking, MIME sniffing |
| express-mongo-sanitize | NoSQL injection ($gt, $where operators) |
| HPP | HTTP parameter pollution |
| Custom CSRF header | Cross-site request forgery |
| SameSite=Lax cookies | CSRF on cross-site requests |
| HttpOnly cookies | XSS-based token theft |
| Redis rate limiting (3 tiers) | Brute force, credential stuffing |
| Zod validation | Malformed and malicious input |
| validateObjectId middleware | MongoDB CastError crashes |
Every incoming request passes through multiple middleware layers before reaching business logic. Each layer removes a specific attack vector such as XSS, CSRF, NoSQL injection, brute-force attacks, malformed input, or unauthorized access.
These are deliberate tradeoffs, not oversights:
| Limitation | Impact | Fix |
|---|---|---|
| No MongoDB transactions on cascade delete | Project delete is not atomic across 3 collections | Wrap in session.startTransaction() |
| Refresh tokens not stored | Can't revoke individual sessions immediately | Store in Redis, delete to revoke |
| Regex search (no text index) | Full collection scan on every search | Add $text index or Atlas Search |
| No Socket.io Redis adapter | Rooms break across multiple server instances | Add @socket.io/redis-adapter |
| No optimistic locking on task reorder | Concurrent drags = last write wins | Add version field, reject stale writes |
| Variable | Where to get it |
|---|---|
PORT |
Choose any available port (default: 5000) |
NODE_ENV |
Set to development locally, production on server |
MONGODB_URI |
MongoDB Atlas β Connect β Drivers β copy connection string |
REDIS_URL |
Upstash β Database β Details tab β Redis Connect URL (rediss://) |
JWT_ACCESS_SECRET |
Generate: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))" |
JWT_REFRESH_SECRET |
Same command, different value |
CLIENT_URL |
Your frontend URL (http://localhost:5173 locally) |
GOOGLE_CLIENT_ID |
Google Cloud Console β APIs & Services β Credentials β OAuth 2.0 Client |
GOOGLE_CLIENT_SECRET |
Same as above |
GOOGLE_CALLBACK_URL |
Must match exactly what you set in Google Console |
The server emits to scoped rooms β not broadcast.
| Room | Joined when | Events received |
|---|---|---|
project:{id} |
User opens a Kanban board | task:created, task:updated, task:deleted, task:reordered |
workspace:{id} |
User enters any workspace page | member:joined, member:removed |
user:{id} |
User connects (automatic) | notification:new |
Every task update is scoped to Socket.io rooms. Only users currently viewing the relevant project receive updates, eliminating unnecessary broadcasts.
npm run dev # Start with nodemon (hot reload)
npm start # Production start
npm run lint # ESLint check
# Seeder
node src/seeders/seed.js # Add demo data
node src/seeders/seed.js --clear # Clear all + reseednpm run dev # Vite dev server with HMR
npm run build # Production build
npm run preview # Preview production build
npm run lint # ESLint check- Set all environment variables in the dashboard
- Set
NODE_ENV=production - Set
CLIENT_URLto your deployed frontend URL - Update
GOOGLE_CALLBACK_URLto your production backend URL - In Google Cloud Console, add the production callback URL to authorized redirect URIs
- Set
VITE_API_URLto your deployed backend URL - Build command:
npm run build - Output directory:
dist
Immediate (know exactly how to build):
- MongoDB transactions for cascade deletes
- Refresh token rotation with Redis storage
- MongoDB text index for proper full-text search
Medium term:
- Email notifications via BullMQ queue + nodemailer
- File attachments on tasks (S3 presigned URLs)
- Task comments system with real-time updates
- Activity feed / audit log
Longer term:
- Socket.io Redis adapter for multi-instance support
- Elasticsearch or Atlas Search for ranked full-text search
- API versioning (
/api/v1/,/api/v2/) - Integration test suite (Jest + Supertest)
MIT License β see LICENSE for details.
Built as a production-inspired full-stack portfolio project to explore the engineering challenges behind modern project management platforms like Jira.
This project showcases secure authentication, workspace-based multi-tenancy, role-based access control (RBAC), real-time collaboration with Socket.io, scalable REST APIs, MongoDB data modeling, Redis-backed rate limiting, and modern React development.
If you found this project helpful or learned something from it, consider giving it a β to support the project.




