Built for CDI Software Engineering Internship 2026
AgFlow is a production-grade, full-stack multi-tenant task management platform built to demonstrate proficiency in scalable web applications, RESTful API design, and relational database architecture.
Multiple organisations (tenants) can use the application simultaneously with complete data isolation — Tenant A can never access Tenant B's data, enforced at the database query level via middleware on every single request.
Every feature in this project maps directly to a skill from the CDI Software Engineering Intern role — not as a checklist exercise, but as a working, deployed system:
| Requirement | How AgFlow demonstrates it |
|---|---|
| Full-Stack Development | React 19 frontend + Node.js/Express backend |
| Relational Databases | PostgreSQL schema via Prisma ORM with foreign-key constraints and indexed queries |
| Authentication Systems | JWT auth with Bcrypt hashing + automated temporary password generation via email |
| Performance Optimisation | Optimistic UI on the Kanban board — zero-latency drag interactions |
| Cloud & Docker | Containerised with Docker Compose locally, live API deployed to cloud |
| System Design | Logical data isolation, RBAC middleware, role promotion approval workflow |
URL: https://agflow-saas.vercel.app
Demo Video: Watch on Google Drive
Demo Credentials (pre-seeded):
| Tenant | Role | Password | |
|---|---|---|---|
| Acme Corp | Super Admin | admin@acme.com | password123 |
| Acme Corp | Developer | dev@acme.com | password123 |
| Globex Inc | Super Admin | admin@globex.com | password123 |
Isolation demo: Log in as
admin@acme.comandadmin@globex.comin two separate incognito windows simultaneously. Every task, project, and audit entry is completely invisible across tenants.
┌─────────────────────────────────────────────────────┐
│ React Frontend │
│ (Vite + Tailwind 4 + Framer Motion) │
└────────────────────┬────────────────────────────────┘
│ HTTP / REST
┌────────────────────▼────────────────────────────────┐
│ Express.js API Gateway │
│ JWT Auth → Tenant Resolver → Role Check │
│ (Middleware Chain) │
└──────┬───────────────┬───────────────┬──────────────┘
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌─────▼───────┐
│ Auth Service│ │ Task Service│ │ Analytics │
│ JWT + Bcrypt│ │ CRUD + Audit│ │ Burndown │
│ Nodemailer │ │ Optimistic │ │ Dashboard │
└──────┬──────┘ └──────┬──────┘ └─────┬───────┘
│ │ │
┌──────▼───────────────▼───────────────▼──────────────┐
│ PostgreSQL Database │
│ Row-level tenantId isolation │
│ Prisma ORM + Migrations │
└─────────────────────────────────────────────────────┘
Every table in the database has a tenantId column. When a user logs in, their tenantId is embedded in the JWT payload. Every API request passes through a middleware chain that:
- Verifies the JWT signature
- Extracts
tenantIdfrom the token - Attaches it to
req.user.tenantId - Forces every Prisma query to scope to
where: { tenantId: req.user.tenantId }
It is architecturally impossible for a user to access another tenant's data — even if they know a resource ID belonging to a different organisation.
Three roles with distinct permissions, enforced at the middleware layer before requests ever reach a controller:
| Permission | SUPER_ADMIN | PROJECT_MANAGER | DEVELOPER |
|---|---|---|---|
| Create / Delete Projects | ✅ | ❌ | ❌ |
| Invite Team Members | ✅ | ❌ | ❌ |
| Create Tasks | ✅ | ✅ | ❌ |
| Assign Tasks | ✅ | ✅ | ❌ |
| Update Task Status | ✅ | ✅ | ✅ |
| Add Comments | ✅ | ✅ | ✅ |
| Request Role Promotion | ❌ | ❌ | ✅ |
| Approve / Reject Promotions | ✅ | ❌ | ❌ |
| View Audit Log | ✅ | ❌ | ❌ |
| View Sprint Analytics | ✅ | ✅ | ❌ |
| Feature | Details |
|---|---|
| 🏢 Multi-tenant Architecture | Complete data isolation via JWT-scoped tenantId on every query |
| 🔐 RBAC | Three roles — SUPER_ADMIN, PROJECT_MANAGER, DEVELOPER — enforced by middleware |
| ✅ Task Management | Create, assign, update, delete — with priorities (HIGH / MEDIUM / LOW) and status tracking |
| 📁 Project Management | Organise tasks into projects, each scoped to a tenant |
These features go beyond the internship brief and demonstrate production-level engineering thinking.
DEVELOPER ──► requests promotion ──► SUPER_ADMIN (email alert)
│
┌─────────▼──────────┐
│ Approve / Reject │
└─────────┬──────────┘
│
JWT permissions updated dynamically
- Developer submits a promotion request from their dashboard
- Super Admin receives an automated email notification via Nodemailer
- Admin approves or rejects with one click — role and JWT payload update instantly
- No manual database edits required
Real email alert sent to Super Admin via Nodemailer — triggered when a Developer requests PROJECT_MANAGER promotion
Developer view — same page, different permissions. Role dropdowns hidden, promotion request shows "Waiting for Approval"
Admin clicks "Invite User"
│
▼
Backend generates secure 8-char random password
│
▼
Bcrypt hashes it → stored in DB
│
▼
Nodemailer (SMTP) emails plain-text credentials to invitee
│
▼
User logs in → prompted to change password
Every mutation in the workspace writes a permanent, append-only record:
| Field | Example Value |
|---|---|
actor |
user_abc123 |
action |
TASK_STATUS_CHANGED |
entityType |
Task |
entityId |
task_xyz789 |
timestamp |
2026-04-24T10:32:00Z |
User drags card React state updates instantly (0ms delay)
│ │
│ ▼
│ UI shows new position
│
▼
PATCH /api/tasks/:id (async, background)
│
├── Success → nothing changes, already correct
└── Failure → card snaps back to original column
- Built with
@dnd-kitfor accessible, performant drag interactions - Perceived latency = zero — no waiting for network
- Graceful rollback on API failure keeps data consistent
| Feature | Details |
|---|---|
| 🗂️ Kanban Board | Drag-and-drop columns (To Do → In Progress → Done) via @dnd-kit |
| 📉 Sprint Burndown Chart | Chart.js — plots ideal vs actual task completion velocity |
| 🔀 List / Kanban Toggle | Switch views without losing state |
| 🎯 Priority Filters | Filter tasks by HIGH, MEDIUM, LOW instantly |
| 🌙 Glassmorphism Dark UI | Tailwind CSS 4 + Framer Motion — smooth, premium transitions |
| 📱 Fully Responsive | Optimised for desktop and mobile screen sizes |
| Feature | Details |
|---|---|
| 🔑 JWT Authentication | Stateless sessions — tenantId + role embedded in token payload |
| 🔐 Bcrypt Password Hashing | Industry-standard hashing with configurable salt rounds |
| 📨 Nodemailer SMTP | Transactional emails for invites and role-change alerts |
| 🛡️ Input Validation | All POST / PATCH routes validated before touching controllers |
| 🚨 Global Error Handler | Consistent { success, message, code } shape across all errors |
| 🚦 Rate Limiting | express-rate-limit on auth routes — blocks brute force attacks |
| Feature | Details |
|---|---|
| 🐳 Docker Compose | docker compose up — spins up app + database in one command |
| 🏭 Production Config | Separate docker-compose.prod.yml for production deployments |
| ☁️ Live Deployment | Frontend live at agflow-saas.vercel.app |
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Tenants │ │ Users │ │ Projects │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ id (PK) │──┐ │ id (PK) │ ┌───│ id (PK) │
│ name │ │ │ tenantId(FK) │───┘ │ tenantId(FK) │
│ slug │ └────│ email │ │ name │
│ createdAt │ │ passwordHash │ │ description │
└──────────────┘ │ role │ │ createdAt │
│ createdAt │ └──────┬───────┘
└──────┬───────┘ │
│ ┌──────▼───────┐
│ │ Tasks │
┌──────▼───────┐ ├──────────────┤
│ AuditLogs │ │ id (PK) │
├──────────────┤ │ tenantId(FK) │
│ id (PK) │ │ projectId(FK)│
│ tenantId(FK) │ │ assigneeId │
│ actorId (FK) │ │ title │
│ action │ │ status │
│ entityType │ │ priority │
│ entityId │ │ createdAt │
│ timestamp │ └──────┬───────┘
└──────────────┘ │
┌──────▼───────┐
│ Comments │
├──────────────┤
│ id (PK) │
│ taskId (FK) │
│ authorId(FK) │
│ content │
│ createdAt │
└──────────────┘
| Layer | Technology |
|---|---|
| Frontend | React 19, Vite, TypeScript |
| Styling | Tailwind CSS 4, Framer Motion |
| Charts | Chart.js |
| Drag & Drop | @dnd-kit |
| Backend | Node.js, Express.js |
| ORM | Prisma |
| Database | PostgreSQL (prod) / SQLite (local dev) |
| Auth | JWT + Bcrypt |
| Nodemailer (SMTP) | |
| Deployment | Vercel (frontend) |
| DevOps | Docker, Docker Compose |
git clone https://github.com/Kv-Logics/agflow-saas.git
cd agflow-saas
cp server/.env.example server/.env
docker compose upApp runs at http://localhost:5173
Backend:
cd server
npm install
cp .env.example .env
# Fill in your PostgreSQL connection string and SMTP credentials in .env
npx prisma migrate dev
npm run seed
npm run devFrontend:
cd client
npm install
npm run devRunning npm run seed in the server directory creates:
- 2 tenants: Acme Corp and Globex Inc
- 4 users across both tenants covering all 3 roles
- Sample projects and tasks in various statuses (To Do, In Progress, Done)
- Ready-to-run data for the isolation proof demo
| Method | URL | Min Role | Description |
|---|---|---|---|
| POST | /api/auth/register | Public | Create new tenant + admin user |
| POST | /api/auth/login | Public | Login, receive JWT |
| POST | /api/auth/invite | SUPER_ADMIN | Generate temp password and email new user |
| Method | URL | Min Role | Description |
|---|---|---|---|
| POST | /api/auth/role/:id | DEVELOPER | Request role promotion to Project Manager |
| POST | /api/approvals/:id/resolve | SUPER_ADMIN | Approve or reject a promotion request |
| Method | URL | Min Role | Description |
|---|---|---|---|
| GET | /api/tasks | DEVELOPER | List all tasks for the tenant |
| POST | /api/tasks | PROJECT_MANAGER | Create a new task |
| PATCH | /api/tasks/:id | DEVELOPER | Update task status (supports optimistic UI) |
| DELETE | /api/tasks/:id | SUPER_ADMIN | Delete a task |
| Method | URL | Min Role | Description |
|---|---|---|---|
| GET | /api/projects | DEVELOPER | List all projects for the tenant |
| POST | /api/projects | SUPER_ADMIN | Create a new project |
| Method | URL | Min Role | Description |
|---|---|---|---|
| GET | /api/tasks/history/audit | SUPER_ADMIN | Fetch the immutable audit log |
| PATCH | /api/users/:id/role | SUPER_ADMIN | Directly update a user's role |
Why tenantId over schema-per-tenant?
Schema-per-tenant provides the strongest isolation but requires a dynamic migration job every time a new organisation registers, significantly increasing operational complexity. For this scale, tenantId with strict middleware enforcement delivers the same security guarantee with far simpler infrastructure. The codebase is structured so PostgreSQL Row-Level Security (RLS) can be added as a second isolation layer for enterprise deployments.
Why Prisma over raw SQL?
Type safety from schema definition through to query execution means a tenant isolation error — such as a missing tenantId filter — surfaces as a compile-time error rather than a live data leak. Automated migration history also keeps schema changes version-controlled alongside application code.
Why JWT over server-side sessions?
Stateless authentication means the API can scale horizontally without a shared session store. Embedding tenantId and role directly in the token payload makes every request self-contained and eliminates an extra database lookup per request.
Scaling path:
Application-level tenantId filtering → PostgreSQL Row-Level Security as a database-layer guarantee → Redis token caching to reduce auth overhead → schema-per-tenant for enterprise customers requiring strict DB-level isolation.
agflow-saas/
├── client/ # React frontend
│ ├── src/
│ │ ├── components/ # Reusable UI components
│ │ ├── pages/ # Route-level page components
│ │ ├── hooks/ # Custom React hooks
│ │ └── lib/ # API client, utilities
├── server/ # Express backend
│ ├── src/
│ │ ├── routes/ # Express route definitions
│ │ ├── controllers/ # Request handlers
│ │ ├── services/ # Business logic
│ │ ├── middleware/ # Auth, tenant resolver, RBAC checks
│ │ └── prisma/ # Schema and migrations
├── docker-compose.yml # Local dev stack
├── docker-compose.prod.yml # Production stack
└── project_summary.txt # Project overview
Built with ⚡ for the CDI Software Engineering Internship 2026






