A secure, highly available task-management platform deployed on AWS, demonstrating cloud architecture, RBAC, event-driven processing, serverless workloads, monitoring, and secure network design.
Live Demo β’ Demo Video β’ Architecture β’ Security
Demo Availability To minimize AWS costs, compute resources may occasionally be offline. If the live application is unavailable, the complete deployment and high-availability demonstration can be viewed in the demo video.
| Role | Password | Access | |
|---|---|---|---|
| Manager | ali@minijira.com | Password123! | All teams, create/assign tasks, audit log |
| Employee (Frontend) | sara@minijira.com | Password123! | Frontend team tasks only |
| Employee (Backend) | omar@minijira.com | Password123! | Backend team tasks only |
Note on Diagrams: We originally designed our architecture diagram in draw.io to ensure 100% technical accuracy. To make it visually stunning for our demo, we enhanced it using an AI tool (which introduced a few minor text typos like the AZ labels). Both versions are provided below for transparency!
- 2 Availability Zones (eu-north-1a, eu-north-1b) for fault tolerance
- Auto Scaling Group (Min:2, Desired:2, Max:4) β self-healing infrastructure
- Application Load Balancer distributes traffic across instances with
/api/healthchecks - CloudFront CDN for global low-latency delivery (static assets cached,
/api/*bypassed) - Private subnets for EC2 instances β no direct public internet access
- 1 NAT Gateway in AZ-A for cost-optimized outbound internet access (HA option: 2 NAT Gateways at double the cost)
| Security Measure | Implementation |
|---|---|
| Private subnets for EC2 | No direct public internet access to backend |
| Cognito JWT validation | Every API route validates token via middleware |
| Team isolation (server-side) | DynamoDB GSI enforces cross-team data access |
| Security Groups | ALB: 443 from internet. EC2: 80 from ALB only |
| IAM least-privilege | EC2 instance profile; Lambda roles scope to specific actions |
| Presigned S3 URLs | Upload URLs expire in 5 min; View URLs expire in 1 hour |
| No credentials in code | All secrets via environment variables on EC2 |
| A+ Security Grade | Achieved an 'A' on SecurityHeaders.com using a CloudFront Custom Response Headers Policy to enforce HSTS, Content-Security-Policy, X-Frame-Options, and X-Content-Type-Options |
- AWS Cognito User Pool with custom attributes:
custom:role,custom:teamId - JWT tokens validated on every API request via middleware
- Two roles: Manager (full access) and Employee (team-scoped)
- Employees see only their team's tasks
- Manager sees all tasks with team filter dropdown
- DynamoDB GSI query on
teamIdenforces cross-team access prevention at the data layer
- Drag-and-drop Kanban board (To Do β In Progress β In Review β Done)
- Every status change logged to
MiniJira_AuditLogDynamoDB table - Comments with full CRUD (Create, Read, Update, Delete)
- Image attachments on tasks
Task Assignment β SNS Topic (MiniJira-TaskAssignment)
βββ (a) Email Subscription β Assignee notification email
βββ (b) SQS Queue (MiniJira-AssignmentQueue)
βββ Assignment Worker Lambda
βββ Writes audit record to DynamoDB AuditLog
βββ Publishes CloudWatch custom metric (AssignmentsProcessed)
User uploads image β S3 PUT (minijira-originals-ali-605856)
β S3 Event triggers Image Resize Lambda
β Sharp library resizes to 400Γ400 JPEG (quality: 85)
β Saves thumbnail to S3 Resized Bucket (minijira-resized-ali-605856)
β Frontend fetches thumbnail via presigned URL (expires 1 hour)
- Runs every day at 9:00 AM UTC via cron
(0 9 * * ?) - Paginated DynamoDB Scan for tasks due today or overdue
- Groups tasks by assignee, sends personalized summary email via SNS
- Publishes
OverdueTasksCountmetric to CloudWatch
Dashboard: MiniJira-Overview
| Widget | Metric |
|---|---|
| Lambda Invocations | All 3 functions |
| Lambda Errors | Spike detection |
| S3 Storage Growth | Both buckets |
| SQS Message Traffic | AssignmentQueue |
| Custom: TasksCreated | Per team |
| Custom: TasksClosed | Time-to-close |
| Custom: OverdueTasksCount | Daily |
Alarms (3 configured):
MiniJira-ImageResizer-Errorsβ fires if Lambda errors > 1 in 5 minMiniJira-AssignmentWorker-Errorsβ fires if Lambda errors > 1 in 5 minMiniJira-SQS-OldMessagesβ fires if messages stuck > 5 min
- React 18 + Vite β fast SPA build
- Vanilla CSS β custom glassmorphism dark theme
- @dnd-kit β drag-and-drop Kanban board
- TanStack Query (React Query) β server state management
- Amazon Cognito SDK β authentication and JWT management
- Node.js 20 + Express β REST API
- AWS SDK v3 β DynamoDB, S3, SNS, SQS, CloudWatch
- PM2 β process management and auto-restart
- Nginx β reverse proxy and static file server
- VPC, Public/Private Subnets, NAT Gateway, Internet Gateway
- EC2 Auto Scaling Group, Application Load Balancer
- CloudFront CDN
- DynamoDB (6 tables, 2 GSIs)
- S3 (2 buckets)
- Lambda (3 functions)
- SNS + SQS
- EventBridge
- Cognito
- CloudWatch + Alarms
- IAM (least-privilege roles)
| # | Service | Role |
|---|---|---|
| 1 | EC2 (Auto Scaling) | Hosts Node.js backend across 2 AZs |
| 2 | Application Load Balancer | Distributes traffic, health checks /api/health |
| 3 | CloudFront | CDN β caches static assets, bypasses cache for /api/* |
| 4 | VPC | Isolated private network (10.0.0.0/16) |
| 5 | Public Subnets (x2) | ALB and NAT Gateway placement |
| 6 | Private Subnets (x2) | EC2 instances β no direct internet access |
| 7 | NAT Gateway | Outbound internet for EC2 (GitHub clone, npm install) |
| 8 | DynamoDB | Users, Teams, Projects, Tasks, Comments, AuditLog |
| 9 | S3 (Originals) | Raw task image attachments |
| 10 | S3 (Resized) | 400Γ400 JPEG thumbnails |
| 11 | Lambda β Image Resize | S3 PUT trigger β Sharp resize β Resized bucket |
| 12 | Lambda β Assignment Worker | SQS consumer β audit log β email β metrics |
| 13 | Lambda β Daily Digest | EventBridge trigger β scan tasks β email digest |
| 14 | SNS | Fan-out notifications for assignments |
| 15 | SQS | Decoupled queue for assignment processing |
| 16 | EventBridge | Cron scheduler β daily digest at 9:00 AM UTC |
| 17 | Cognito | User authentication, JWT, custom:role, custom:teamId |
| 18 | CloudWatch | Dashboard, 6 custom metrics, 3 alarms |
| 19 | IAM | EC2 Instance Profile + Lambda execution roles |
Mini-JiraAWS/
βββ client/ # React frontend (Vite)
β βββ src/
β βββ components/
β β βββ kanban/ # Kanban board, TaskCard, TaskDetailModal
β β βββ layout/ # Sidebar, MainLayout
β βββ pages/ # Dashboard, Tasks, Analytics, Projects, Teams
β βββ context/ # AuthContext (Cognito)
β βββ lib/ # API client
βββ server/ # Express backend (Node.js 20)
β βββ src/
β βββ routes/ # API endpoints (tasks, teams, projects, etc.)
β βββ services/ # Database and AWS services
β βββ middleware/ # Auth and team isolation
β βββ config/ # Environment & SDK configurations
βββ infrastructure/ # AWS configuration & IaC scripts
βββ lambda/
β βββ image-resize/ # Serverless image processing (sharp)
β βββ assignment-worker/ # SQS consumer (audit log + email)
β βββ daily-digest/ # EventBridge cron scheduled notifications
βββ cloudwatch/ # Dashboard and custom alarm scripts
βββ sns/ # SNS topic configuration
βββ sqs/ # SQS queue creation
βββ s3-sns/ # S3 bucket and trigger setup
βββ users/ # Cognito test users creation script
5-Day Total Deployment Cost: ~$8β10
| Resource | Cost |
|---|---|
| NAT Gateway | $5.40 (5 days Γ $1.08/day) |
| EC2 (above free tier) | ~$2.50 |
| All other services | ~$0 (free tier / pay-per-use) |
Strategies Applied:
- β 1 NAT Gateway instead of 2 (saves ~$32/month; HA tradeoff accepted for cost)
- β ASG desired = 0 when not testing (stops EC2 billing)
- β DynamoDB On-Demand β pay per request, zero idle cost
- β Lambda β only charged per invocation, not idle
Problem: Installing sharp on Windows produced binaries incompatible with Lambda's Linux runtime.
Solution: Used AWS CloudShell (Linux) to install and package sharp, ensuring correct glibc-linked binaries.
Problem: Lambda received SQS records where the body was a JSON string containing another JSON string (SNS envelope wrapping). Caused SyntaxError: Unexpected token.
Solution: Double JSON parse with graceful fallback:
const snsMessage = JSON.parse(record.body); // SQS body = SNS envelope
const taskData = JSON.parse(snsMessage.Message); // SNS message = actual task dataProblem: CloudFront cached API calls, causing users to see stale task data.
Solution: Created a dedicated cache behavior for /api/* path with Managed-CachingDisabled policy, while keeping CachingOptimized for static React assets.
Problem: TaskDetailModal.jsx fetched image URLs from http://localhost:5000/api/upload/view-url/.... In production, browsers couldn't resolve localhost.
Solution: Changed to relative path /api/upload/view-url/... β Nginx on EC2 proxies to the Express server.
Problem: Backend view-url endpoint generated presigned URLs from the Originals bucket instead of the Resized bucket, causing large file transfers.
Solution: Updated server/src/routes/upload.js to read from S3_BUCKETS.RESIZED.
Demo covers:
- Architecture diagram walkthrough
- Manager creates and assigns task with image upload
- Employee login β team isolation in action
- Drag-and-drop Kanban status update
- Image thumbnail display (Lambda resize pipeline)
- CloudWatch dashboard
- Live high-availability test β stop one EC2, app keeps running, ASG auto-replaces
- GitHub Repository: https://github.com/3l2i/NotJira
- Live Application: https://d9lm1us8lvrs3.cloudfront.net
- Demo Video: https://youtu.be/T-qR7sVdsVs
- AWS Region: eu-north-1 (Stockholm)


