// Case study
Comet Project Pipeline - Project Management System
A project-management platform for PT Koperasi Metropolitan — qualification tracking, document control, and task delegation in one system of record. Built in five months by a six-person team I led; the outcome on record is a 20% gain in development efficiency.
Table of Contents
PT Koperasi Metropolitan needed qualification tracking, controlled documentation, and task delegation in one system of record. This is the engagement where I led the build: six people, five months, delivered through PT ACSA, and live with the client.
The brief
The cooperative asked for one system that could answer three questions its existing tooling could not: which qualifications a project carries and which it is missing, which version of a document is the one that counts, and who owns a given task right now. Around those sat the expected management surface — project dashboards, milestone and timeline views, resource allocation, and reporting.
The baseline is legible in the brief itself. An organization does not ask for centralized document storage, a complete audit trail on qualification changes, and automated alerts for missing qualifications unless records are scattered, history is reconstructed by hand, and gaps surface late. And a generic task tool would not carry the qualification model — qualifications attached to projects, with compliance history — which is why this was a build, not a purchase.
Constraints
- Team. Six people including me: two front-end engineers, two back-end engineers, one DevOps engineer, with me leading and contributing full-stack. Experience levels varied, so consistency had to come from process rather than from assuming everyone writes the same code.
- Window. Five months from kickoff to a live system, with no separate hardening phase budgeted. Quality enforcement had to run continuously, not arrive at the end.
- Hosting. The client's own infrastructure: Docker on a VM behind Nginx, processes supervised by PM2. No managed cloud, no Kubernetes, no platform team on call — whatever we shipped, this team had to operate.
- Domain. Qualification tracking is compliance work. Every change needed an audit trail, and the rights to create projects and assign work had to be restricted to admin and project-manager roles.
- Load profile. An internal tool, but with concurrent users on dashboards over data that only grows — documents, tasks, and qualification history accumulate for the life of a project.
Approach & decisions
The architecture follows the constraints more than any stylistic preference. Each decision below answers a problem the brief or the hosting reality put on the table.
A modular monolith, with the seams drawn
The fashionable answer in 2023 was microservices; the deployment target was a single VM under PM2. I considered a service split along the obvious boundaries — projects, tasks, documents, users — and rejected it: a six-person team on client-operated infrastructure cannot staff the operational surface of a distributed system. We shipped one NestJS application with those same boundaries enforced as modules instead. The seams are drawn, so a future split is a refactor rather than a rewrite — and one process under PM2 is a deployment the client can actually run.
Live boards over polling
Task delegation is the product's heartbeat, and a stale board is precisely the failure the tool exists to remove. I considered short-polling — stateless, simpler to operate — and rejected it in favor of a WebSocket gateway with a room per project: task updates push only to the people viewing that project, which keeps fan-out bounded on a single node.
Quality as a gate, not a policy
With varying experience levels and no hardening phase, code quality could not
live in a document people agree to. ESLint and Prettier set a non-negotiable
baseline; pair programming and review covered every merge; regular technical
workshops spread context instead of concentrating it in me. In CI, unit and E2E
suites run with a coverage check on every push, and deployment to the client VM
happens only from main after the full suite passes.
Indexes and caches where the queries actually were
Dashboards over growing datasets slowed as concurrent usage picked up during
delivery. Rather than reaching for a generic caching layer, we matched composite
indexes to the query shapes the UI actually issues — [projectId, status] for
boards, [assigneeId] for my-tasks views, [startDate, endDate] for timeline
ranges — put cache-aside Redis with a five-minute TTL on hot project aggregates,
and made pagination the default for every unbounded list.
Evidence
Leveled deliberately: system facts stay system facts, and stated outcomes are presented as exactly that.
- The figure on record for this engagement is a 20% gain in development efficiency. That is the team's stated outcome from the time — a self-assessment of delivery pace after the standards-and-CI regime settled in — not an independently audited measurement, and I present it as such.
- System facts: every deploy gated in CI behind unit suites, E2E suites, and a coverage check; role-based access control enforced at the route layer; an audit trail on qualification changes; indexed, paginated queries behind every dashboard view.
- Design goals: built to keep dashboards responsive under concurrent internal load (cache-aside reads, bounded WebSocket fan-out), and designed so that "the current version" of a document is never ambiguous (versioned records behind role-gated access).
- Delivery: the six-person team shipped inside the five-month window, and the system is live with the client.
- What I do not claim: post-handover uptime, adoption, or usage figures. Measurement after handover was not mine to run, so no number of that kind appears here.
Stack notes
NestJS and TypeScript over PostgreSQL through Prisma; Redis for cache-aside reads; a WebSocket gateway for live boards. The dashboard is Next.js (App Router) with Redux Toolkit for client state and React Query for server state. Notifications fan out to email, in-app, and Slack. Docker images are built in GitHub Actions and served behind Nginx under PM2 on the client VM. Sensitive fields are encrypted at rest (AES-256), passwords are hashed with bcrypt, transport is HTTPS-only, and mutating routes sit behind role guards.
The index strategy is the part worth showing — boring on purpose, mirroring the screens users actually sit on:
model Task {
id String @id @default(cuid())
title String
status TaskStatus @default(TODO)
priority Priority @default(MEDIUM)
dueDate DateTime?
projectId String
project Project @relation(fields: [projectId], references: [id])
assigneeId String?
assignee User? @relation(fields: [assigneeId], references: [id])
@@index([projectId, status])
@@index([assigneeId])
}
Nothing in this stack is exotic. The discipline was in the gates, not the tools.