Version: 1.0.0
Last Updated: November 2025
Status: Architecture Design Phase
- Executive Summary
- Technology Stack
- Project Structure
- System Architecture
- Database Schema
- API Design
- Authentication & Authorization
- Frontend Architecture
- Real-Time Features
- Analytics System
- Docker Strategy
- CI/CD Pipeline
- Technology Decisions
- Security Considerations
- Deployment Architecture
- Scalability & Performance
- Development Workflow
TaskForge is an enterprise-grade project management application designed to showcase modern full-stack development practices. The application enables teams to collaborate on tasks with real-time updates, role-based access control, and comprehensive analytics.
- ✅ Multi-user team collaboration
- ✅ Real-time task updates via WebSockets
- ✅ Role-based access control (Admin, Team Lead, Member)
- ✅ Advanced analytics and reporting
- ✅ Containerized deployment
- ✅ CI/CD automation
- ✅ Scalable cloud architecture
- Modularity: Clean separation of concerns
- Scalability: Horizontal scaling capability
- Maintainability: Well-documented, testable code
- Security: Industry-standard authentication and authorization
- Performance: Optimized for real-time interactions
- Developer Experience: Efficient development workflow
- Framework: NestJS 10.x (TypeScript)
- Runtime: Node.js 20.x LTS
- Database ORM: TypeORM 0.3.x
- Database: PostgreSQL 15.x
- Authentication: Passport.js + JWT
- Real-time: Socket.io
- Validation: class-validator, class-transformer
- API Documentation: Swagger/OpenAPI
- Testing: Jest, Supertest
- Framework: React 18.x (TypeScript)
- Build Tool: Vite 5.x
- UI Library: Material-UI (MUI) v5
- State Management: Zustand
- Routing: React Router v6
- HTTP Client: Axios
- Real-time: Socket.io-client
- Charts: Chart.js + react-chartjs-2
- Form Management: React Hook Form + Yup
- Testing: Vitest, React Testing Library
- Containerization: Docker, Docker Compose
- CI/CD: GitHub Actions
- Cloud Provider: AWS
- Compute: ECS (Elastic Container Service) or EC2
- Database: RDS (PostgreSQL)
- Load Balancer: Application Load Balancer (ALB)
- Storage: S3 (for static assets)
- Container Registry: ECR
- Reverse Proxy: Nginx
- Monitoring: CloudWatch (AWS)
- Environment Management: dotenv
- Code Quality: ESLint, Prettier
- Git Hooks: Husky
- Documentation: Markdown, Swagger UI
- API Testing: Postman/Thunder Client
TaskForge/
├── .github/
│ └── workflows/
│ ├── backend-ci.yml
│ ├── frontend-ci.yml
│ └── deploy.yml
│
├── backend/ # NestJS Application
│ ├── src/
│ │ ├── auth/ # Authentication module
│ │ │ ├── guards/ # Auth guards (JWT, Roles)
│ │ │ ├── strategies/ # Passport strategies
│ │ │ ├── decorators/ # Custom decorators
│ │ │ ├── dto/ # Data transfer objects
│ │ │ ├── auth.controller.ts
│ │ │ ├── auth.service.ts
│ │ │ └── auth.module.ts
│ │ │
│ │ ├── users/ # Users module
│ │ │ ├── entities/ # User entity
│ │ │ ├── dto/
│ │ │ ├── users.controller.ts
│ │ │ ├── users.service.ts
│ │ │ └── users.module.ts
│ │ │
│ │ ├── teams/ # Teams module
│ │ │ ├── entities/ # Team, TeamMember entities
│ │ │ ├── dto/
│ │ │ ├── teams.controller.ts
│ │ │ ├── teams.service.ts
│ │ │ └── teams.module.ts
│ │ │
│ │ ├── tasks/ # Tasks module
│ │ │ ├── entities/ # Task entity
│ │ │ ├── dto/
│ │ │ ├── tasks.controller.ts
│ │ │ ├── tasks.service.ts
│ │ │ ├── tasks.gateway.ts # WebSocket gateway
│ │ │ └── tasks.module.ts
│ │ │
│ │ ├── analytics/ # Analytics module
│ │ │ ├── entities/ # ActivityLog entity
│ │ │ ├── dto/
│ │ │ ├── analytics.controller.ts
│ │ │ ├── analytics.service.ts
│ │ │ └── analytics.module.ts
│ │ │
│ │ ├── notifications/ # Notifications module
│ │ │ ├── notifications.gateway.ts
│ │ │ ├── notifications.service.ts
│ │ │ └── notifications.module.ts
│ │ │
│ │ ├── common/ # Shared utilities
│ │ │ ├── decorators/
│ │ │ ├── filters/ # Exception filters
│ │ │ ├── interceptors/ # Response interceptors
│ │ │ ├── pipes/ # Validation pipes
│ │ │ └── types/ # Shared types/enums
│ │ │
│ │ ├── database/ # Database configuration
│ │ │ ├── migrations/
│ │ │ ├── seeds/
│ │ │ └── database.module.ts
│ │ │
│ │ ├── config/ # Configuration
│ │ │ ├── app.config.ts
│ │ │ ├── database.config.ts
│ │ │ ├── jwt.config.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── app.module.ts # Root module
│ │ └── main.ts # Application entry
│ │
│ ├── test/ # E2E tests
│ ├── .env.example
│ ├── .eslintrc.js
│ ├── .prettierrc
│ ├── nest-cli.json
│ ├── tsconfig.json
│ ├── package.json
│ └── Dockerfile
│
├── frontend/ # React Application
│ ├── src/
│ │ ├── components/ # Reusable components
│ │ │ ├── common/ # Generic components
│ │ │ │ ├── Button/
│ │ │ │ ├── Input/
│ │ │ │ ├── Modal/
│ │ │ │ ├── Loader/
│ │ │ │ └── ErrorBoundary/
│ │ │ │
│ │ │ ├── layout/ # Layout components
│ │ │ │ ├── Header/
│ │ │ │ ├── Sidebar/
│ │ │ │ ├── Footer/
│ │ │ │ └── MainLayout/
│ │ │ │
│ │ │ ├── tasks/ # Task components
│ │ │ │ ├── TaskCard/
│ │ │ │ ├── TaskForm/
│ │ │ │ ├── TaskList/
│ │ │ │ ├── TaskDetail/
│ │ │ │ └── TaskFilters/
│ │ │ │
│ │ │ ├── teams/ # Team components
│ │ │ │ ├── TeamCard/
│ │ │ │ ├── TeamForm/
│ │ │ │ ├── TeamMemberList/
│ │ │ │ └── MemberInvite/
│ │ │ │
│ │ │ └── analytics/ # Analytics components
│ │ │ ├── ChartWrapper/
│ │ │ ├── StatCard/
│ │ │ ├── BurndownChart/
│ │ │ ├── ProductivityChart/
│ │ │ └── CompletionTrends/
│ │ │
│ │ ├── pages/ # Page components
│ │ │ ├── Auth/
│ │ │ │ ├── Login.tsx
│ │ │ │ ├── Register.tsx
│ │ │ │ └── ForgotPassword.tsx
│ │ │ ├── Dashboard/
│ │ │ │ └── Dashboard.tsx
│ │ │ ├── Tasks/
│ │ │ │ ├── TasksPage.tsx
│ │ │ │ └── TaskDetailPage.tsx
│ │ │ ├── Teams/
│ │ │ │ ├── TeamsPage.tsx
│ │ │ │ └── TeamDetailPage.tsx
│ │ │ ├── Analytics/
│ │ │ │ └── AnalyticsPage.tsx
│ │ │ └── Profile/
│ │ │ └── ProfilePage.tsx
│ │ │
│ │ ├── services/ # API services
│ │ │ ├── api.ts # Axios instance
│ │ │ ├── auth.service.ts
│ │ │ ├── users.service.ts
│ │ │ ├── teams.service.ts
│ │ │ ├── tasks.service.ts
│ │ │ ├── analytics.service.ts
│ │ │ └── socket.service.ts
│ │ │
│ │ ├── stores/ # Zustand stores
│ │ │ ├── authStore.ts
│ │ │ ├── tasksStore.ts
│ │ │ ├── teamsStore.ts
│ │ │ ├── notificationsStore.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── hooks/ # Custom hooks
│ │ │ ├── useAuth.ts
│ │ │ ├── useTasks.ts
│ │ │ ├── useTeams.ts
│ │ │ ├── useSocket.ts
│ │ │ ├── usePermissions.ts
│ │ │ └── useAnalytics.ts
│ │ │
│ │ ├── types/ # TypeScript types
│ │ │ ├── auth.types.ts
│ │ │ ├── user.types.ts
│ │ │ ├── team.types.ts
│ │ │ ├── task.types.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── utils/ # Utility functions
│ │ │ ├── formatters.ts
│ │ │ ├── validators.ts
│ │ │ ├── constants.ts
│ │ │ └── helpers.ts
│ │ │
│ │ ├── routes/ # Route configuration
│ │ │ ├── ProtectedRoute.tsx
│ │ │ ├── RoleBasedRoute.tsx
│ │ │ └── routes.tsx
│ │ │
│ │ ├── theme/ # MUI theme
│ │ │ ├── theme.ts
│ │ │ └── overrides.ts
│ │ │
│ │ ├── App.tsx # Root component
│ │ ├── main.tsx # Application entry
│ │ └── vite-env.d.ts
│ │
│ ├── public/
│ ├── .env.example
│ ├── .eslintrc.cjs
│ ├── .prettierrc
│ ├── tsconfig.json
│ ├── vite.config.ts
│ ├── package.json
│ └── Dockerfile
│
├── docker-compose.yml # Local development
├── docker-compose.prod.yml # Production deployment
├── nginx.conf # Nginx configuration
├── .gitignore
├── .prettierrc
├── .editorconfig
├── README.md
└── ARCHITECTURE.md # This document
┌─────────────────────────────────────────────────────────────────────┐
│ CLIENT LAYER │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ React Application (Vite) │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ │
│ │ │ Pages │ │ Components │ │ Zustand Stores │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────────────┘ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ │
│ │ │ HTTP Client │ │ Socket.io │ │ Chart.js Visualizer │ │ │
│ │ │ (Axios) │ │ Client │ │ │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
│ HTTPS/WSS
▼
┌─────────────────────────────────────────────────────────────────────┐
│ LOAD BALANCER (AWS ALB) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ REVERSE PROXY (Nginx) │
│ SSL Termination │ Request Routing │ Static Assets │
└─────────────────────────────────────────────────────────────────────┘
│ │
│ │
┌───────────────┴────────────┐ │
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐
│ NestJS API │ │ WebSocket │ │ Static │
│ (REST API) │◄────────►│ Gateway │ │ Assets │
│ │ │ (Socket.io) │ │ (S3/Nginx) │
└─────────────────┘ └─────────────────┘ └──────────────┘
│ │
│ ┌──────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ Auth │ │ Teams │ │ Tasks │ │ Analytics │ │
│ │ Module │ │ Module │ │ Module │ │ Module │ │
│ └────────────┘ └────────────┘ └────────────┘ └──────────────┘ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ Users │ │Notifications│ │ Common │ │ Database │ │
│ │ Module │ │ Module │ │ Module │ │ Module │ │
│ └────────────┘ └────────────┘ └────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
│ TypeORM
▼
┌─────────────────────────────────────────────────────────────────────┐
│ DATABASE LAYER (AWS RDS) │
│ PostgreSQL 15.x (Primary) │
│ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────────┐ ┌─────────────────┐ │
│ │Users │ │Teams │ │Tasks │ │TeamMembers│ │ ActivityLogs │ │
│ └──────┘ └──────┘ └──────┘ └──────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Client → ALB → Nginx → NestJS Controller → Service → Repository → PostgreSQL
│
PostgreSQL → Repository → Service → Controller → Nginx → ALB → Client ←┘
Client Action → HTTP Request → NestJS Service → Database Update
│
├──→ Emit WebSocket Event
│
All Connected Clients ←───────────────┘
Login Request → Auth Controller → Auth Service → Validate Credentials
│
▼
Generate JWT Access Token
Generate JWT Refresh Token
│
▼
Client Storage ←────────────────── Return Tokens & User Data
Subsequent Requests:
Request + JWT → JWT Guard → Validate Token → Extract User → Allow Access
┌─────────────────────┐
│ Users │
├─────────────────────┤
│ id (PK) │◄─────────────┐
│ email (unique) │ │
│ username (unique) │ │
│ password_hash │ │
│ first_name │ │
│ last_name │ │
│ avatar_url │ │
│ created_at │ │
│ updated_at │ │
└─────────────────────┘ │
│ │
│ 1 │
│ │
│ N │
│ │
┌────────▼────────────┐ │
│ TeamMembers │ │
├─────────────────────┤ │
│ id (PK) │ │
│ team_id (FK) │───┐ │
│ user_id (FK) │ │ │
│ role (enum) │ │ │
│ joined_at │ │ │
└─────────────────────┘ │ │
│ │ │
│ N │ N │
│ │ │
│ 1 │ │
│ │ │
┌────────▼────────────┐ │ │
│ Teams │◄──┘ │
├─────────────────────┤ │
│ id (PK) │ │
│ name │ │
│ description │ │
│ owner_id (FK) │──────────────┘
│ created_at │
│ updated_at │
└─────────────────────┘
│
│ 1
│
│ N
│
┌────────▼────────────┐
│ Tasks │
├─────────────────────┤
│ id (PK) │
│ title │
│ description │
│ status (enum) │
│ priority (enum) │
│ team_id (FK) │
│ assigned_to (FK) │──────┐
│ created_by (FK) │──────┼────────┐
│ due_date │ │ │
│ completed_at │ │ │
│ created_at │ │ │
│ updated_at │ │ │
└─────────────────────┘ │ │
│ │ │
│ 1 │ N │ N
│ │ │
│ N │ │
│ │ │
┌────────▼────────────┐ │ │
│ ActivityLogs │ │ │
├─────────────────────┤ │ │
│ id (PK) │ │ │
│ task_id (FK) │ │ │
│ user_id (FK) │──────┴────────┘
│ action_type (enum) │
│ old_value (json) │
│ new_value (json) │
│ created_at │
└─────────────────────┘
{
id: UUID PRIMARY KEY,
email: VARCHAR(255) UNIQUE NOT NULL,
username: VARCHAR(50) UNIQUE NOT NULL,
password_hash: VARCHAR(255) NOT NULL,
first_name: VARCHAR(100),
last_name: VARCHAR(100),
avatar_url: VARCHAR(500),
created_at: TIMESTAMP DEFAULT NOW(),
updated_at: TIMESTAMP DEFAULT NOW()
}
// Indexes
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);{
id: UUID PRIMARY KEY,
name: VARCHAR(100) NOT NULL,
description: TEXT,
owner_id: UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at: TIMESTAMP DEFAULT NOW(),
updated_at: TIMESTAMP DEFAULT NOW()
}
// Indexes
CREATE INDEX idx_teams_owner ON teams(owner_id);{
id: UUID PRIMARY KEY,
team_id: UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
user_id: UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role: ENUM('admin', 'team_lead', 'member') DEFAULT 'member',
joined_at: TIMESTAMP DEFAULT NOW(),
CONSTRAINT unique_team_user UNIQUE(team_id, user_id)
}
// Indexes
CREATE INDEX idx_team_members_team ON team_members(team_id);
CREATE INDEX idx_team_members_user ON team_members(user_id);
CREATE INDEX idx_team_members_role ON team_members(role);{
id: UUID PRIMARY KEY,
title: VARCHAR(255) NOT NULL,
description: TEXT,
status: ENUM('todo', 'in_progress', 'in_review', 'completed', 'blocked') DEFAULT 'todo',
priority: ENUM('low', 'medium', 'high', 'urgent') DEFAULT 'medium',
team_id: UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
assigned_to: UUID REFERENCES users(id) ON DELETE SET NULL,
created_by: UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
due_date: TIMESTAMP,
completed_at: TIMESTAMP,
created_at: TIMESTAMP DEFAULT NOW(),
updated_at: TIMESTAMP DEFAULT NOW()
}
// Indexes
CREATE INDEX idx_tasks_team ON tasks(team_id);
CREATE INDEX idx_tasks_assigned ON tasks(assigned_to);
CREATE INDEX idx_tasks_created_by ON tasks(created_by);
CREATE INDEX idx_tasks_status ON tasks(status);
CREATE INDEX idx_tasks_priority ON tasks(priority);
CREATE INDEX idx_tasks_due_date ON tasks(due_date);
CREATE INDEX idx_tasks_created_at ON tasks(created_at);{
id: UUID PRIMARY KEY,
task_id: UUID REFERENCES tasks(id) ON DELETE CASCADE,
user_id: UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
action_type: ENUM(
'task_created',
'task_updated',
'task_deleted',
'task_assigned',
'status_changed',
'priority_changed',
'comment_added'
) NOT NULL,
old_value: JSONB,
new_value: JSONB,
created_at: TIMESTAMP DEFAULT NOW()
}
// Indexes
CREATE INDEX idx_activity_logs_task ON activity_logs(task_id);
CREATE INDEX idx_activity_logs_user ON activity_logs(user_id);
CREATE INDEX idx_activity_logs_type ON activity_logs(action_type);
CREATE INDEX idx_activity_logs_created ON activity_logs(created_at);- Users ↔ Teams: Many-to-Many via TeamMembers
- Teams → Tasks: One-to-Many (cascade delete)
- Users → Tasks (assigned_to): One-to-Many (set null on delete)
- Users → Tasks (created_by): One-to-Many (cascade delete)
- Tasks → ActivityLogs: One-to-Many (cascade delete)
- Users → ActivityLogs: One-to-Many (cascade delete)
| Method | Endpoint | Description | Request Body | Response |
|---|---|---|---|---|
| POST | /api/auth/register |
Register new user | { email, username, password, firstName, lastName } |
{ user, tokens } |
| POST | /api/auth/login |
Login user | { email, password } |
{ user, tokens } |
| POST | /api/auth/refresh |
Refresh access token | { refreshToken } |
{ accessToken } |
| POST | /api/auth/logout |
Logout user | - | { success: true } |
| GET | /api/auth/me |
Get current user | - | { user } |
| Method | Endpoint | Description | Access | Response |
|---|---|---|---|---|
| GET | /api/users/profile |
Get own profile | Authenticated | { user } |
| PATCH | /api/users/profile |
Update own profile | Authenticated | { user } |
| POST | /api/users/upload-avatar |
Upload avatar | Authenticated | { avatarUrl } |
| GET | /api/users/:id |
Get user by ID | Team Member | { user } |
| Method | Endpoint | Description | Access | Response |
|---|---|---|---|---|
| GET | /api/teams |
Get user's teams | Authenticated | { teams[] } |
| POST | /api/teams |
Create team | Authenticated | { team } |
| GET | /api/teams/:id |
Get team details | Team Member | { team } |
| PATCH | /api/teams/:id |
Update team | Admin/Owner | { team } |
| DELETE | /api/teams/:id |
Delete team | Owner | { success: true } |
| GET | /api/teams/:id/members |
Get team members | Team Member | { members[] } |
| POST | /api/teams/:id/members |
Add team member | Admin/Team Lead | { member } |
| PATCH | /api/teams/:id/members/:userId |
Update member role | Admin | { member } |
| DELETE | /api/teams/:id/members/:userId |
Remove member | Admin | { success: true } |
| Method | Endpoint | Description | Access | Response |
|---|---|---|---|---|
| GET | /api/tasks |
Get all team tasks | Team Member | { tasks[], pagination } |
| POST | /api/tasks |
Create task | Team Member | { task } |
| GET | /api/tasks/:id |
Get task details | Team Member | { task } |
| PATCH | /api/tasks/:id |
Update task | Assignee/Creator/Admin | { task } |
| DELETE | /api/tasks/:id |
Delete task | Creator/Admin | { success: true } |
| PATCH | /api/tasks/:id/status |
Update status only | Assignee/Admin | { task } |
| PATCH | /api/tasks/:id/assign |
Assign task to user | Team Lead/Admin | { task } |
Query Parameters for GET /api/tasks:
teamId(required): Filter by teamstatus: Filter by status (todo, in_progress, in_review, completed, blocked)priority: Filter by priority (low, medium, high, urgent)assignedTo: Filter by assigned user IDsearch: Search in title and descriptionsortBy: Sort field (createdAt, dueDate, priority, status)sortOrder: asc or descpage: Page number (default: 1)limit: Items per page (default: 20, max: 100)
| Method | Endpoint | Description | Access | Response |
|---|---|---|---|---|
| GET | /api/analytics/team/:teamId/overview |
Team overview stats | Team Member | { stats } |
| GET | /api/analytics/team/:teamId/completion-trends |
Completion trends | Team Member | { trends[] } |
| GET | /api/analytics/team/:teamId/productivity |
Team productivity | Team Lead/Admin | { productivity[] } |
| GET | /api/analytics/team/:teamId/burndown |
Sprint burndown | Team Member | { burndown[] } |
| GET | /api/analytics/user/:userId/stats |
User statistics | Self/Admin | { stats } |
Analytics Response Examples:
// Team Overview
{
totalTasks: 150,
completedTasks: 95,
inProgressTasks: 35,
blockedTasks: 5,
todoTasks: 15,
completionRate: 63.3,
overdueTasksCount: 8,
averageCompletionTime: 4.5, // days
tasksByPriority: { low: 30, medium: 80, high: 30, urgent: 10 }
}
// Completion Trends (last 30 days)
[
{ date: '2025-01-01', completed: 5, created: 8 },
{ date: '2025-01-02', completed: 3, created: 5 },
...
]
// Team Productivity
[
{ userId: 'uuid', username: 'john_doe', tasksCompleted: 25, avgCompletionTime: 3.2 },
...
]
// Burndown Chart
{
sprintStart: '2025-01-01',
sprintEnd: '2025-01-14',
totalPoints: 100,
dataPoints: [
{ date: '2025-01-01', remaining: 100, ideal: 100 },
{ date: '2025-01-02', remaining: 93, ideal: 92.8 },
...
]
}Success Response:
{
success: true,
data: { ... },
message?: string
}Error Response:
{
success: false,
error: {
code: 'ERROR_CODE',
message: 'Human readable message',
details?: { ... }
}
}Paginated Response:
{
success: true,
data: [...],
pagination: {
page: 1,
limit: 20,
total: 150,
totalPages: 8,
hasNext: true,
hasPrev: false
}
}JWT-based Authentication with Refresh Tokens
-
Access Token: Short-lived (15 minutes)
- Contains: userId, email, username
- Used for API requests
- Stored in memory or state management
-
Refresh Token: Long-lived (7 days)
- Contains: userId, tokenId
- Used to obtain new access tokens
- Stored in httpOnly cookie (secure)
System Admin (Future)
│
└── Team Owner
│
├── Team Admin
│ │
│ └── Team Lead
│ │
│ └── Member
| Role | Team Creation | Team Settings | Add Members | Remove Members | Task CRUD | Assign Tasks | View Analytics |
|---|---|---|---|---|---|---|---|
| Owner | ✅ | ✅ Full | ✅ | ✅ All | ✅ | ✅ | ✅ Full |
| Admin | ❌ | ✅ Partial | ✅ | ✅ (not owner/admin) | ✅ | ✅ | ✅ Full |
| Team Lead | ❌ | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ Team Only |
| Member | ❌ | ❌ | ❌ | ❌ | ✅ Own tasks | ❌ | ✅ Personal Only |
// Permissions enum
enum Permission {
// Team permissions
TEAM_CREATE = 'team:create',
TEAM_UPDATE = 'team:update',
TEAM_DELETE = 'team:delete',
TEAM_VIEW = 'team:view',
// Member permissions
MEMBER_ADD = 'member:add',
MEMBER_REMOVE = 'member:remove',
MEMBER_UPDATE_ROLE = 'member:update_role',
MEMBER_VIEW = 'member:view',
// Task permissions
TASK_CREATE = 'task:create',
TASK_UPDATE_ANY = 'task:update:any',
TASK_UPDATE_OWN = 'task:update:own',
TASK_DELETE_ANY = 'task:delete:any',
TASK_DELETE_OWN = 'task:delete:own',
TASK_ASSIGN = 'task:assign',
TASK_VIEW = 'task:view',
// Analytics permissions
ANALYTICS_TEAM = 'analytics:team',
ANALYTICS_USER = 'analytics:user'
}
// Role → Permissions mapping
const RolePermissions = {
owner: [
Permission.TEAM_UPDATE,
Permission.TEAM_DELETE,
Permission.MEMBER_ADD,
Permission.MEMBER_REMOVE,
Permission.MEMBER_UPDATE_ROLE,
Permission.TASK_CREATE,
Permission.TASK_UPDATE_ANY,
Permission.TASK_DELETE_ANY,
Permission.TASK_ASSIGN,
Permission.ANALYTICS_TEAM,
Permission.ANALYTICS_USER
],
admin: [
Permission.TEAM_UPDATE, // partial
Permission.MEMBER_ADD,
Permission.MEMBER_REMOVE, // except owner/admin
Permission.MEMBER_UPDATE_ROLE, // except owner
Permission.TASK_CREATE,
Permission.TASK_UPDATE_ANY,
Permission.TASK_DELETE_ANY,
Permission.TASK_ASSIGN,
Permission.ANALYTICS_TEAM,
Permission.ANALYTICS_USER
],
team_lead: [
Permission.MEMBER_ADD,
Permission.TASK_CREATE,
Permission.TASK_UPDATE_ANY,
Permission.TASK_DELETE_OWN,
Permission.TASK_ASSIGN,
Permission.ANALYTICS_TEAM
],
member: [
Permission.TASK_CREATE,
Permission.TASK_UPDATE_OWN,
Permission.TASK_DELETE_OWN,
Permission.ANALYTICS_USER
]
};// JWT Auth Guard
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
}
// Roles Guard
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<Role[]>('roles', context.getHandler());
if (!requiredRoles) return true;
const request = context.switchToHttp().getRequest();
const user = request.user;
const teamId = request.params.teamId || request.body.teamId;
// Check if user has required role in the team
return this.hasRequiredRole(user, teamId, requiredRoles);
}
}
// Permissions Guard
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredPermissions = this.reflector.get<Permission[]>(
'permissions',
context.getHandler()
);
if (!requiredPermissions) return true;
const request = context.switchToHttp().getRequest();
const user = request.user;
return this.userHasPermissions(user, requiredPermissions);
}
}@Controller('tasks')
@UseGuards(JwtAuthGuard, RolesGuard)
export class TasksController {
@Post()
@Roles(Role.MEMBER, Role.TEAM_LEAD, Role.ADMIN, Role.OWNER)
@RequirePermissions(Permission.TASK_CREATE)
createTask(@Body() createTaskDto: CreateTaskDto) {
// Only team members and above can create tasks
}
@Delete(':id')
@Roles(Role.ADMIN, Role.OWNER)
@RequirePermissions(Permission.TASK_DELETE_ANY)
deleteTask(@Param('id') id: string) {
// Only admins and owners can delete any task
}
}Atomic Design Pattern
Atoms (Basic Building Blocks)
├── Button
├── Input
├── Badge
├── Avatar
└── Icon
Molecules (Simple Components)
├── FormField (Label + Input + Error)
├── SearchBar (Input + Icon + Button)
├── StatCard (Icon + Title + Value)
└── UserChip (Avatar + Name + Badge)
Organisms (Complex Components)
├── TaskCard
├── TaskForm
├── TeamMemberList
├── BurndownChart
└── Header/Sidebar
Templates (Page Layouts)
├── MainLayout (Header + Sidebar + Content)
├── AuthLayout (Centered Form)
└── DashboardLayout (Stats + Charts + Tables)
Pages (Specific Instances)
├── Dashboard
├── TasksPage
├── TeamsPage
└── AnalyticsPage
Store Architecture
// stores/authStore.ts
interface AuthState {
user: User | null;
accessToken: string | null;
isAuthenticated: boolean;
isLoading: boolean;
// Actions
login: (email: string, password: string) => Promise<void>;
register: (data: RegisterDto) => Promise<void>;
logout: () => void;
refreshToken: () => Promise<void>;
updateProfile: (data: Partial<User>) => Promise<void>;
}
// stores/tasksStore.ts
interface TasksState {
tasks: Task[];
currentTask: Task | null;
filters: TaskFilters;
isLoading: boolean;
pagination: Pagination;
// Actions
fetchTasks: (teamId: string, filters?: TaskFilters) => Promise<void>;
createTask: (data: CreateTaskDto) => Promise<Task>;
updateTask: (id: string, data: UpdateTaskDto) => Promise<Task>;
deleteTask: (id: string) => Promise<void>;
setFilters: (filters: Partial<TaskFilters>) => void;
}
// stores/teamsStore.ts
interface TeamsState {
teams: Team[];
currentTeam: Team | null;
members: TeamMember[];
isLoading: boolean;
// Actions
fetchTeams: () => Promise<void>;
fetchTeamDetails: (teamId: string) => Promise<void>;
createTeam: (data: CreateTeamDto) => Promise<Team>;
addMember: (teamId: string, userId: string, role: Role) => Promise<void>;
updateMemberRole: (teamId: string, userId: string, role: Role) => Promise<void>;
}
// stores/notificationsStore.ts
interface NotificationsState {
notifications: Notification[];
unreadCount: number;
// Actions
addNotification: (notification: Notification) => void;
markAsRead: (id: string) => void;
clearAll: () => void;
}// routes/routes.tsx
const routes = [
{
path: '/',
element: <MainLayout />,
children: [
{
path: 'dashboard',
element: <ProtectedRoute><Dashboard /></ProtectedRoute>
},
{
path: 'tasks',
element: <ProtectedRoute><TasksPage /></ProtectedRoute>
},
{
path: 'tasks/:id',
element: <ProtectedRoute><TaskDetailPage /></ProtectedRoute>
},
{
path: 'teams',
element: <ProtectedRoute><TeamsPage /></ProtectedRoute>
},
{
path: 'teams/:id',
element: <ProtectedRoute><TeamDetailPage /></ProtectedRoute>
},
{
path: 'analytics',
element: (
<RoleBasedRoute requiredRoles={['team_lead', 'admin', 'owner']}>
<AnalyticsPage />
</RoleBasedRoute>
)
},
{
path: 'profile',
element: <ProtectedRoute><ProfilePage /></ProtectedRoute>
}
]
},
{
path: '/auth',
element: <AuthLayout />,
children: [
{ path: 'login', element: <Login /> },
{ path: 'register', element: <Register /> },
{ path: 'forgot-password', element: <ForgotPassword /> }
]
}
];// hooks/useSocket.ts
const useSocket = (teamId: string) => {
const updateTask = useTasksStore(state => state.updateTask);
const addNotification = useNotificationsStore(state => state.addNotification);
useEffect(() => {
if (!teamId) return;
// Join team room
socket.emit('join:team', teamId);
// Listen for task updates
socket.on('task:updated', (task: Task) => {
updateTask(task);
addNotification({
type: 'info',
message: `Task "${task.title}" was updated`
});
});
socket.on('task:created', (task: Task) => {
// Fetch tasks again or add to local state
});
socket.on('task:deleted', (taskId: string) => {
// Remove from local state
});
return () => {
socket.emit('leave:team', teamId);
socket.off('task:updated');
socket.off('task:created');
socket.off('task:deleted');
};
}, [teamId]);
};| Event | Direction | Payload | Description |
|---|---|---|---|
connection |
Client → Server | { token } |
Initial WebSocket connection |
authenticated |
Server → Client | { userId } |
Authentication successful |
disconnect |
Both | - | Connection closed |
| Event | Direction | Payload | Description |
|---|---|---|---|
join:team |
Client → Server | { teamId } |
Join team room |
leave:team |
Client → Server | { teamId } |
Leave team room |
team:member_joined |
Server → Clients | { teamId, member } |
New member added |
team:member_left |
Server → Clients | { teamId, userId } |
Member removed |
| Event | Direction | Payload | Description |
|---|---|---|---|
task:created |
Server → Clients | { task } |
New task created |
task:updated |
Server → Clients | { task, changes } |
Task updated |
task:deleted |
Server → Clients | { taskId } |
Task deleted |
task:assigned |
Server → Clients | { taskId, assignedTo } |
Task assigned to user |
task:status_changed |
Server → Clients | { taskId, oldStatus, newStatus } |
Status changed |
| Event | Direction | Payload | Description |
|---|---|---|---|
notification:new |
Server → Client | { notification } |
New notification |
notification:read |
Client → Server | { notificationId } |
Mark as read |
// tasks/tasks.gateway.ts
@WebSocketGateway({
cors: { origin: '*' },
namespace: '/ws'
})
export class TasksGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
constructor(
private authService: AuthService,
private tasksService: TasksService
) {}
async handleConnection(client: Socket) {
try {
const token = client.handshake.auth.token;
const user = await this.authService.verifyToken(token);
client.data.user = user;
client.emit('authenticated', { userId: user.id });
} catch (error) {
client.disconnect();
}
}
@SubscribeMessage('join:team')
handleJoinTeam(client: Socket, teamId: string) {
client.join(`team:${teamId}`);
}
@SubscribeMessage('leave:team')
handleLeaveTeam(client: Socket, teamId: string) {
client.leave(`team:${teamId}`);
}
// Emit to team members
emitTaskUpdate(teamId: string, task: Task) {
this.server.to(`team:${teamId}`).emit('task:updated', task);
}
emitTaskCreated(teamId: string, task: Task) {
this.server.to(`team:${teamId}`).emit('task:created', task);
}
}// services/socket.service.ts
class SocketService {
private socket: Socket | null = null;
connect(accessToken: string) {
this.socket = io(WS_URL, {
auth: { token: accessToken },
transports: ['websocket']
});
this.socket.on('connect', () => {
console.log('WebSocket connected');
});
this.socket.on('authenticated', ({ userId }) => {
console.log('Authenticated as:', userId);
});
}
disconnect() {
if (this.socket) {
this.socket.disconnect();
this.socket = null;
}
}
joinTeam(teamId: string) {
this.socket?.emit('join:team', teamId);
}
leaveTeam(teamId: string) {
this.socket?.emit('leave:team', teamId);
}
on(event: string, callback: Function) {
this.socket?.on(event, callback);
}
off(event: string) {
this.socket?.off(event);
}
}
export default new SocketService();-
Overview Statistics
- Total tasks (all time)
- Tasks by status (todo, in_progress, completed, etc.)
- Tasks by priority
- Completion rate (%)
- Overdue tasks count
- Average completion time
-
Completion Trends
- Daily task completions (last 30 days)
- Weekly task completions (last 12 weeks)
- Monthly task completions (last 12 months)
- Tasks created vs completed over time
-
Team Productivity
- Tasks completed per team member
- Average completion time per member
- Tasks in progress per member
- Activity heat map
-
Sprint Burndown
- Total story points/tasks
- Remaining work over time
- Ideal burndown line
- Velocity tracking
- Personal task completion stats
- Personal productivity trends
- Tasks assigned vs completed
- Average time to complete tasks
// analytics/analytics.service.ts
export class AnalyticsService {
async getTeamOverview(teamId: string): Promise<TeamOverviewDto> {
const tasks = await this.tasksRepository.find({
where: { teamId },
relations: ['assignedTo', 'createdBy']
});
const completed = tasks.filter(t => t.status === 'completed');
const overdue = tasks.filter(t =>
t.dueDate && new Date(t.dueDate) < new Date() && t.status !== 'completed'
);
return {
totalTasks: tasks.length,
completedTasks: completed.length,
inProgressTasks: tasks.filter(t => t.status === 'in_progress').length,
todoTasks: tasks.filter(t => t.status === 'todo').length,
blockedTasks: tasks.filter(t => t.status === 'blocked').length,
completionRate: (completed.length / tasks.length) * 100,
overdueTasksCount: overdue.length,
averageCompletionTime: this.calculateAvgCompletionTime(completed),
tasksByPriority: this.groupByPriority(tasks)
};
}
async getCompletionTrends(
teamId: string,
period: 'daily' | 'weekly' | 'monthly',
limit: number = 30
): Promise<TrendDataPoint[]> {
// Query activity logs grouped by date
const trends = await this.activityLogsRepository
.createQueryBuilder('log')
.select('DATE(log.created_at) as date')
.addSelect('COUNT(*) as count')
.where('log.action_type = :action', { action: 'task_created' })
.orWhere('log.action_type = :action', { action: 'status_changed' })
.groupBy('DATE(log.created_at)')
.orderBy('date', 'DESC')
.limit(limit)
.getRawMany();
return trends;
}
async getTeamProductivity(teamId: string): Promise<UserProductivityDto[]> {
const members = await this.teamMembersRepository.find({
where: { teamId },
relations: ['user']
});
const productivity = await Promise.all(
members.map(async (member) => {
const completedTasks = await this.tasksRepository.count({
where: {
teamId,
assignedTo: member.userId,
status: 'completed'
}
});
const avgCompletionTime = await this.calculateUserAvgCompletionTime(
member.userId,
teamId
);
return {
userId: member.userId,
username: member.user.username,
tasksCompleted: completedTasks,
avgCompletionTime
};
})
);
return productivity;
}
}// components/analytics/BurndownChart.tsx
import { Line } from 'react-chartjs-2';
export const BurndownChart: React.FC<{ teamId: string }> = ({ teamId }) => {
const [burndownData, setBurndownData] = useState<BurndownData | null>(null);
useEffect(() => {
analyticsService.getBurndownData(teamId).then(setBurndownData);
}, [teamId]);
const chartData = {
labels: burndownData?.dataPoints.map(d => d.date) || [],
datasets: [
{
label: 'Remaining Tasks',
data: burndownData?.dataPoints.map(d => d.remaining) || [],
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
},
{
label: 'Ideal Burndown',
data: burndownData?.dataPoints.map(d => d.ideal) || [],
borderColor: 'rgb(255, 99, 132)',
borderDash: [5, 5],
backgroundColor: 'transparent',
}
]
};
const options = {
responsive: true,
plugins: {
legend: { position: 'top' as const },
title: { display: true, text: 'Sprint Burndown Chart' }
},
scales: {
y: { beginAtZero: true }
}
};
return <Line data={chartData} options={options} />;
};# backend/Dockerfile
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY tsconfig*.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY src ./src
# Build application
RUN npm run build
# Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install production dependencies only
RUN npm ci --only=production
# Copy built application from builder
COPY --from=builder /app/dist ./dist
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nestjs -u 1001
USER nestjs
EXPOSE 3000
CMD ["node", "dist/main"]# frontend/Dockerfile
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY . .
# Build application
ARG VITE_API_URL
ENV VITE_API_URL=$VITE_API_URL
RUN npm run build
# Stage 2: Production with Nginx
FROM nginx:alpine AS production
# Copy built assets
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]# docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: taskforge-db
environment:
POSTGRES_DB: taskforge
POSTGRES_USER: taskforge_user
POSTGRES_PASSWORD: taskforge_password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- taskforge-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U taskforge_user"]
interval: 10s
timeout: 5s
retries: 5
backend:
build:
context: ./backend
dockerfile: Dockerfile
target: development
container_name: taskforge-backend
environment:
NODE_ENV: development
DATABASE_HOST: postgres
DATABASE_PORT: 5432
DATABASE_NAME: taskforge
DATABASE_USER: taskforge_user
DATABASE_PASSWORD: taskforge_password
JWT_SECRET: ${JWT_SECRET}
JWT_EXPIRES_IN: 15m
REFRESH_TOKEN_EXPIRES_IN: 7d
ports:
- "3000:3000"
volumes:
- ./backend/src:/app/src
- /app/node_modules
depends_on:
postgres:
condition: service_healthy
networks:
- taskforge-network
command: npm run start:dev
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
target: development
container_name: taskforge-frontend
environment:
VITE_API_URL: http://localhost:3000
VITE_WS_URL: ws://localhost:3000
ports:
- "5173:5173"
volumes:
- ./frontend/src:/app/src
- /app/node_modules
depends_on:
- backend
networks:
- taskforge-network
command: npm run dev -- --host
networks:
taskforge-network:
driver: bridge
volumes:
postgres_data:# docker-compose.prod.yml
version: '3.8'
services:
nginx:
image: nginx:alpine
container_name: taskforge-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
- static_files:/usr/share/nginx/html
depends_on:
- backend
- frontend
networks:
- taskforge-network
restart: unless-stopped
backend:
build:
context: ./backend
dockerfile: Dockerfile
target: production
container_name: taskforge-backend
environment:
NODE_ENV: production
DATABASE_HOST: ${DB_HOST}
DATABASE_PORT: ${DB_PORT}
DATABASE_NAME: ${DB_NAME}
DATABASE_USER: ${DB_USER}
DATABASE_PASSWORD: ${DB_PASSWORD}
JWT_SECRET: ${JWT_SECRET}
JWT_EXPIRES_IN: 15m
REFRESH_TOKEN_EXPIRES_IN: 7d
networks:
- taskforge-network
restart: unless-stopped
deploy:
replicas: 2
resources:
limits:
cpus: '1'
memory: 512M
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
target: production
args:
VITE_API_URL: ${API_URL}
container_name: taskforge-frontend
volumes:
- static_files:/usr/share/nginx/html
networks:
- taskforge-network
restart: unless-stopped
networks:
taskforge-network:
driver: bridge
volumes:
static_files:# nginx.conf
upstream backend {
server backend:3000;
}
server {
listen 80;
server_name taskforge.com www.taskforge.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name taskforge.com www.taskforge.com;
# SSL Configuration
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Frontend static files
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# API proxy
location /api {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# WebSocket proxy
location /ws {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
}# .github/workflows/backend-ci.yml
name: Backend CI/CD
on:
push:
branches: [ main, develop ]
paths:
- 'backend/**'
- '.github/workflows/backend-ci.yml'
pull_request:
branches: [ main, develop ]
paths:
- 'backend/**'
jobs:
test:
name: Test Backend
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_DB: taskforge_test
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_password
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: backend/package-lock.json
- name: Install dependencies
working-directory: backend
run: npm ci
- name: Run linter
working-directory: backend
run: npm run lint
- name: Run unit tests
working-directory: backend
run: npm run test:cov
env:
DATABASE_HOST: localhost
DATABASE_PORT: 5432
DATABASE_NAME: taskforge_test
DATABASE_USER: test_user
DATABASE_PASSWORD: test_password
JWT_SECRET: test-secret-key
- name: Run e2e tests
working-directory: backend
run: npm run test:e2e
env:
DATABASE_HOST: localhost
DATABASE_PORT: 5432
DATABASE_NAME: taskforge_test
DATABASE_USER: test_user
DATABASE_PASSWORD: test_password
JWT_SECRET: test-secret-key
- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
files: ./backend/coverage/lcov.info
flags: backend
build:
name: Build Backend Docker Image
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push image to ECR
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: taskforge-backend
IMAGE_TAG: ${{ github.sha }}
working-directory: backend
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
deploy:
name: Deploy to AWS ECS
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Update ECS service
run: |
aws ecs update-service \
--cluster taskforge-cluster \
--service taskforge-backend-service \
--force-new-deployment
- name: Wait for deployment to complete
run: |
aws ecs wait services-stable \
--cluster taskforge-cluster \
--services taskforge-backend-service# .github/workflows/frontend-ci.yml
name: Frontend CI/CD
on:
push:
branches: [ main, develop ]
paths:
- 'frontend/**'
- '.github/workflows/frontend-ci.yml'
pull_request:
branches: [ main, develop ]
paths:
- 'frontend/**'
jobs:
test:
name: Test Frontend
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Run linter
working-directory: frontend
run: npm run lint
- name: Run type check
working-directory: frontend
run: npm run type-check
- name: Run unit tests
working-directory: frontend
run: npm run test:coverage
- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
files: ./frontend/coverage/lcov.info
flags: frontend
build:
name: Build and Deploy Frontend
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Build application
working-directory: frontend
run: npm run build
env:
VITE_API_URL: ${{ secrets.VITE_API_URL }}
VITE_WS_URL: ${{ secrets.VITE_WS_URL }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Deploy to S3
working-directory: frontend/dist
run: |
aws s3 sync . s3://${{ secrets.S3_BUCKET }} --delete
- name: Invalidate CloudFront cache
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/*"┌─────────────────────────────────────────────────────────────┐
│ Developer Workflow │
└─────────────────────────────────────────────────────────────┘
│
▼
[ git push to main ]
│
┌─────────────────────┴──────────────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Backend CI/CD │ │ Frontend CI/CD │
├──────────────────┤ ├──────────────────┤
│ 1. Lint │ │ 1. Lint │
│ 2. Test │ │ 2. Type Check │
│ 3. Build Docker │ │ 3. Test │
│ 4. Push to ECR │ │ 4. Build │
│ 5. Deploy to ECS │ │ 5. Deploy to S3 │
└──────────────────┘ └──────────────────┘
│ │
└─────────────────────┬──────────────────────┘
▼
[ Application Deployed ]
| Criteria | NestJS | Express |
|---|---|---|
| Architecture | ✅ Built-in modular architecture | ❌ Requires manual setup |
| TypeScript | ✅ First-class support | |
| Dependency Injection | ✅ Built-in DI container | ❌ Manual implementation |
| Testing | ✅ Testing utilities included | |
| WebSockets | ✅ Native Socket.io integration | |
| API Documentation | ✅ Swagger integration | |
| Scalability | ✅ Enterprise-ready patterns | |
| Learning Curve | ✅ Minimal |
Decision: NestJS provides better structure for a portfolio project demonstrating enterprise development skills.
| Criteria | Zustand | Redux Toolkit | Context API |
|---|---|---|---|
| Boilerplate | ✅ Minimal | ✅ Minimal | |
| Performance | ✅ Excellent | ✅ Excellent | |
| DevTools | ✅ Redux DevTools support | ✅ Native DevTools | ❌ No DevTools |
| Learning Curve | ✅ Easy | ✅ Easy | |
| Bundle Size | ✅ ~1KB | ✅ 0KB (built-in) | |
| TypeScript | ✅ Excellent | ✅ Excellent |
Decision: Zustand offers the best balance of simplicity, performance, and developer experience for this project.
| Criteria | TypeORM | Prisma | Sequelize |
|---|---|---|---|
| TypeScript Support | ✅ Native | ✅ Excellent | |
| NestJS Integration | ✅ Official support | ✅ Good | ✅ Official support |
| Migrations | ✅ Built-in | ✅ Built-in | ✅ Built-in |
| Query Builder | ✅ Powerful | ✅ Good | |
| Performance | ✅ Good | ✅ Excellent | ✅ Good |
| Learning Curve | ✅ Easy |
Decision: TypeORM is the standard ORM in the NestJS ecosystem with excellent TypeScript support.
| Criteria | MUI | Ant Design | Chakra UI |
|---|---|---|---|
| Component Library | ✅ Comprehensive | ✅ Comprehensive | ✅ Good |
| Customization | ✅ Excellent (theming) | ✅ Good | ✅ Excellent |
| Documentation | ✅ Excellent | ✅ Excellent | ✅ Good |
| Enterprise Ready | ✅ Yes | ✅ Yes | |
| Bundle Size | ✅ ~150KB | ||
| TypeScript | ✅ Excellent | ✅ Excellent | ✅ Excellent |
Decision: MUI is the most widely adopted React UI library with excellent Material Design implementation.
- Vite over Create React App: Faster builds, better DX, native ESM
- Jest/Vitest for Testing: Industry standard with excellent TypeScript support
- Socket.io for WebSockets: Reliable, feature-rich, auto-reconnection
- Chart.js: Lightweight, flexible, well-documented
- PostgreSQL over MongoDB: Better for relational data, ACID compliance, mature ecosystem
- Docker: Standard for containerization, portable, consistent environments
- AWS over Heroku/Vercel: More control, scalable, industry standard for portfolio
-
Authentication Security
- ✅ Bcrypt password hashing (10 rounds)
- ✅ JWT with short expiration (15 minutes)
- ✅ Refresh tokens in httpOnly cookies
- ✅ Token rotation on refresh
- ✅ Secure password requirements (8+ chars, mixed case, numbers)
-
Authorization Security
- ✅ Role-based access control (RBAC)
- ✅ Permission-based guards
- ✅ Resource ownership validation
- ✅ Team membership verification
-
API Security
- ✅ Rate limiting (express-rate-limit)
- ✅ Helmet.js for security headers
- ✅ CORS configuration
- ✅ Request validation (class-validator)
- ✅ SQL injection prevention (TypeORM parameterized queries)
- ✅ XSS protection (input sanitization)
-
Data Security
- ✅ Environment variables for secrets
- ✅ Database connection pooling
- ✅ Encrypted database connections (SSL)
- ✅ No sensitive data in logs
- ✅ Secure cookie settings (httpOnly, secure, sameSite)
-
Infrastructure Security
- ✅ HTTPS/TLS encryption
- ✅ Security groups (AWS)
- ✅ Private subnets for database
- ✅ Secrets management (AWS Secrets Manager)
- ✅ Regular security updates (Dependabot)
// main.ts
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", process.env.API_URL],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
noSniff: true,
xssFilter: true,
referrerPolicy: { policy: 'same-origin' }
}));// Rate limiting configuration
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later.'
});
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // Stricter limit for auth endpoints
skipSuccessfulRequests: true
});
app.use('/api/', limiter);
app.use('/api/auth/', authLimiter);┌──────────────────────────────────────────────────────────────┐
│ AWS Cloud │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Route 53 (DNS) │ │
│ └────────────────────┬───────────────────────────────────┘ │
│ │ │
│ ┌────────────────────▼───────────────────────────────────┐ │
│ │ CloudFront (CDN) [Optional] │ │
│ └────────────────────┬───────────────────────────────────┘ │
│ │ │
│ ┌────────────────────▼───────────────────────────────────┐ │
│ │ Application Load Balancer (ALB) │ │
│ │ - SSL/TLS Termination │ │
│ │ - Health Checks │ │
│ └────┬────────────────────────────────┬──────────────────┘ │
│ │ │ │
│ ┌────▼─────────────┐ ┌────▼─────────────┐ │
│ │ Target Group │ │ Target Group │ │
│ │ (Backend) │ │ (Frontend) │ │
│ └────┬─────────────┘ └────┬─────────────┘ │
│ │ │ │
│ ┌────▼──────────────────────────┐ │ │
│ │ ECS Cluster │ │ │
│ │ ┌─────────────────────────┐ │ │ │
│ │ │ ECS Service (Backend) │ │ │ │
│ │ │ - Task Definition │ │ │ │
│ │ │ - Auto Scaling │ │ │ │
│ │ │ - Multiple Tasks │ │ │ │
│ │ └─────────────────────────┘ │ │ │
│ └───────────────────────────────┘ │ │
│ │ │ │
│ ┌─────────────▼──────────────┐ ┌────▼─────────────┐ │
│ │ ECR (Container Registry) │ │ S3 Bucket │ │
│ │ - Backend Images │ │ - Static Files │ │
│ └────────────────────────────┘ └──────────────────┘ │
│ │ │
│ ┌─────────────▼──────────────────────────────┐ │
│ │ RDS PostgreSQL (Multi-AZ) │ │
│ │ - Primary Instance │ │
│ │ - Standby Instance (Failover) │ │
│ │ - Automated Backups │ │
│ └────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────┐ │
│ │ CloudWatch │ │
│ │ - Logs Aggregation │ │
│ │ - Metrics & Monitoring │ │
│ │ - Alarms │ │
│ └───────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────┐ │
│ │ Secrets Manager │ │
│ │ - Database Credentials │ │
│ │ - JWT Secrets │ │
│ │ - API Keys │ │
│ └───────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
| Service | Purpose | Configuration |
|---|---|---|
| Route 53 | DNS management | Domain routing to ALB |
| CloudFront | CDN (optional) | Cache static assets, reduce latency |
| Application Load Balancer | Load balancing | HTTPS termination, health checks |
| ECS (Fargate) | Container orchestration | Run backend containers, auto-scaling |
| ECR | Container registry | Store Docker images |
| S3 | Object storage | Frontend static files |
| RDS PostgreSQL | Database | Multi-AZ deployment, automated backups |
| CloudWatch | Monitoring | Logs, metrics, alarms |
| Secrets Manager | Secrets management | Database credentials, API keys |
| VPC | Network isolation | Public/private subnets, security groups |
{
"family": "taskforge-backend",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::ACCOUNT_ID:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::ACCOUNT_ID:role/ecsTaskRole",
"containerDefinitions": [
{
"name": "backend",
"image": "ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/taskforge-backend:latest",
"portMappings": [
{
"containerPort": 3000,
"protocol": "tcp"
}
],
"environment": [
{ "name": "NODE_ENV", "value": "production" },
{ "name": "PORT", "value": "3000" }
],
"secrets": [
{
"name": "DATABASE_HOST",
"valueFrom": "arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:taskforge/db-host"
},
{
"name": "DATABASE_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:taskforge/db-password"
},
{
"name": "JWT_SECRET",
"valueFrom": "arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:taskforge/jwt-secret"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/taskforge-backend",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 60
}
}
]
}# .env.production
NODE_ENV=production
PORT=3000
# Database (from Secrets Manager)
DATABASE_HOST=taskforge-db.xxxxx.us-east-1.rds.amazonaws.com
DATABASE_PORT=5432
DATABASE_NAME=taskforge
DATABASE_USER=taskforge_admin
DATABASE_PASSWORD=<from-secrets-manager>
# JWT (from Secrets Manager)
JWT_SECRET=<from-secrets-manager>
JWT_EXPIRES_IN=15m
REFRESH_TOKEN_EXPIRES_IN=7d
# CORS
CORS_ORIGIN=https://taskforge.com
# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100-
Horizontal Scaling
- ECS auto-scaling based on CPU/memory
- Multiple task instances behind ALB
- Stateless application design
-
Database Scaling
- Read replicas for read-heavy operations
- Connection pooling (TypeORM)
- Indexes on frequently queried columns
- Pagination for large datasets
-
Caching Strategy
- In-memory caching for frequently accessed data
- Redis for distributed caching (future)
- CDN for static assets
-
Performance Optimizations
- Lazy loading of relationships
- Bulk operations for batch updates
- Query optimization with TypeORM query builder
- Compression middleware (gzip)
-
Code Splitting
- Route-based code splitting
- Lazy loading of components
- Dynamic imports
-
Asset Optimization
- Image optimization (WebP, lazy loading)
- Tree shaking (Vite)
- Minification and compression
- CDN delivery
-
Performance Best Practices
- React.memo for expensive components
- useMemo/useCallback for optimizations
- Virtual scrolling for long lists
- Debouncing for search/filters
// Example: Optimized task query with joins and pagination
async findAllWithPagination(
teamId: string,
filters: TaskFilters,
page: number = 1,
limit: number = 20
): Promise<PaginatedResult<Task>> {
const query = this.taskRepository
.createQueryBuilder('task')
.leftJoinAndSelect('task.assignedTo', 'assignedUser')
.leftJoinAndSelect('task.createdBy', 'createdUser')
.where('task.teamId = :teamId', { teamId })
.orderBy('task.createdAt', 'DESC')
.skip((page - 1) * limit)
.take(limit);
// Apply filters
if (filters.status) {
query.andWhere('task.status = :status', { status: filters.status });
}
if (filters.priority) {
query.andWhere('task.priority = :priority', { priority: filters.priority });
}
if (filters.assignedTo) {
query.andWhere('task.assignedTo = :assignedTo', { assignedTo: filters.assignedTo });
}
const [tasks, total] = await query.getManyAndCount();
return {
data: tasks,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
};
}-
Clone Repository
git clone https://github.com/yourusername/TaskForge.git cd TaskForge -
Environment Setup
# Copy environment files cp backend/.env.example backend/.env cp frontend/.env.example frontend/.env # Edit .env files with your local configuration
-
Start with Docker Compose
docker-compose up -d
-
Access Application
- Frontend: http://localhost:5173
- Backend API: http://localhost:3000
- API Docs: http://localhost:3000/api-docs
main (production)
│
├─── develop (staging)
│ │
│ ├─── feature/user-authentication
│ ├─── feature/task-management
│ ├─── bugfix/login-validation
│ └─── hotfix/security-patch
│
└─── release/v1.0.0
- main: Production-ready code
- develop: Integration branch for features
- feature/*: New features
- bugfix/*: Bug fixes
- hotfix/*: Urgent production fixes
- release/*: Release preparation
<type>(<scope>): <subject>
<body>
<footer>
Types: feat, fix, docs, style, refactor, test, chore
Example:
feat(tasks): add real-time task updates via WebSocket
- Implemented Socket.io gateway for task events
- Added event listeners in frontend
- Updated task store to handle real-time updates
Closes #123
- Create feature branch
- Implement changes
- Write tests
- Create pull request
- Code review (at least 1 approval)
- CI/CD checks pass
- Merge to develop
- Deploy to staging
- QA testing
- Merge to main
- Deploy to production
- Project structure setup
- Backend boilerplate (NestJS)
- Frontend boilerplate (React + Vite)
- Database schema creation
- Docker setup
- CI/CD pipeline basic setup
- User registration/login
- JWT authentication
- Password hashing
- Auth guards and middleware
- Frontend auth flow
- Token refresh mechanism
- Team management (CRUD)
- Team member management
- Task management (CRUD)
- Role-based access control
- Task assignment
- Status/priority management
- WebSocket setup
- Real-time task updates
- Notifications system
- Live team activity feed
- Activity logging
- Team statistics
- Completion trends
- Productivity metrics
- Burndown charts
- Chart.js integration
- Unit tests (backend)
- Unit tests (frontend)
- E2E tests
- Performance optimization
- UI/UX refinements
- Error handling improvements
- AWS infrastructure setup
- Production deployment
- Monitoring setup
- Documentation finalization
- Demo video creation
NODE_ENV=development|production
PORT=3000
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_NAME=taskforge
DATABASE_USER=taskforge_user
DATABASE_PASSWORD=your_password
JWT_SECRET=your-secret-key-min-32-chars
JWT_EXPIRES_IN=15m
REFRESH_TOKEN_EXPIRES_IN=7d
CORS_ORIGIN=http://localhost:5173
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100VITE_API_URL=http://localhost:3000
VITE_WS_URL=ws://localhost:3000
VITE_APP_NAME=TaskForge| Code | HTTP Status | Description |
|---|---|---|
AUTH_001 |
401 | Invalid credentials |
AUTH_002 |
401 | Token expired |
AUTH_003 |
403 | Insufficient permissions |
USER_001 |
404 | User not found |
USER_002 |
409 | Email already exists |
TEAM_001 |
404 | Team not found |
TEAM_002 |
403 | Not a team member |
TASK_001 |
404 | Task not found |
TASK_002 |
403 | Cannot modify task |
VAL_001 |
400 | Validation error |
SRV_001 |
500 | Internal server error |
# Development
npm run dev # Start both frontend and backend
docker-compose up -d # Start with Docker
docker-compose logs -f # View logs
# Backend
cd backend
npm run start:dev # Start in watch mode
npm run test # Run tests
npm run test:e2e # Run E2E tests
npm run migration:generate # Generate migration
npm run migration:run # Run migrations
# Frontend
cd frontend
npm run dev # Start dev server
npm run build # Build for production
npm run preview # Preview production build
npm run test # Run tests
npm run lint # Lint code
# Docker
docker build -t taskforge-backend ./backend
docker build -t taskforge-frontend ./frontend
docker-compose -f docker-compose.prod.yml up -d
# AWS
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com
docker push ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/taskforge-backend:latest
aws ecs update-service --cluster taskforge-cluster --service taskforge-backend-service --force-new-deploymentThis architecture document provides a comprehensive blueprint for building TaskForge, a modern full-stack project management application. The design emphasizes:
- Scalability: Horizontal scaling with containerization and cloud infrastructure
- Maintainability: Modular architecture with clear separation of concerns
- Security: Industry-standard authentication, authorization, and data protection
- Performance: Optimized queries, caching strategies, and real-time capabilities
- Developer Experience: Well-structured codebase, automated testing, and CI/CD
The architecture is production-ready and demonstrates enterprise-level development practices suitable for a portfolio project.
- Review and approve this architecture document
- Set up the project structure
- Begin implementation following the phased approach
- Maintain documentation as the project evolves
Document Version: 1.0.0
Author: Omar Aglan
Date: November 2025
Status: Ready for Implementation