GitHub Copilot Finish-Up-A-Thon Challenge submission β May 21βJune 7, 2026.
π Project Source
- Repository: github.com/AliHaroon111/Project-Management-Backend
- Challenge Branch: copilot-challenge-submission
Every developer has that one folder. The one with a half-built project that got shelved mid-way, full of potential but never shipped. Mine was a Node.js authentication boilerplate β 12 files, a working register endpoint, and 8 silent bugs that made the entire password-reset flow fail without a single error message.
This challenge gave me the perfect reason to open it back up. What I shipped after 4 days with GitHub Copilot is something I'm genuinely proud of.
The Before: An Honest Assessment
Before writing a single line of new code, I ran a full audit on what I actually had.
My initial codebase: 12 files, auth-only, 8 bugs, no task management, no error handling, ~55% complete.
Here is what I found:
What was working (sort of):
- User registration and login β but login only accepted email, ignoring the
usernamefield it was receiving - JWT middleware β but with a dead
{ header }import fromexpress-validatorsitting at the top - Email verification flow β but the verification URL in every email was pointing to
/api/v1/users/verify-email/which didn't exist - Forgot password β completely broken because
forgotPasswordMailgenContentwas imported inauth.controllers.jsbut never actually imported frommail.js
What was silently broken:
The most dangerous bug was in resetForgotPassword. The token lookup was doing:
crypto.createHash("sha-256") // β wrong
The correct algorithm name in Node.js crypto is "sha256" β no hyphen. This meant every single password reset attempt would silently fail to find the user in the database and return "Token is invalid or expired" β even for a valid token that was seconds old. A user would never know why.
Eight bugs total. None of them throwing loud errors. All of them breaking real user flows.
Phase 1: The Git Branching Wall β My First Real Fear
I need to be honest about something before I talk about code.
When I saw the challenge requirement β "work must be done on a separate branch"
β my first reaction was anxiety, not excitement.
Branches. Merging. Checkout. These words had always looked intimidating to me.
I had been using Git for months but only ever on main. One branch.
Push and pray. The mental model of parallel branches, switching between them,
and then merging them back together genuinely confused me. I had avoided it
completely.
The challenge didn't give me that option.
So I sat down, read the docs properly for the first time, and actually understood
what a branch is β it's just a pointer to a commit. A safe copy of your work
where you can build freely without touching the original. That's it.
The terminology had made it sound far more complicated than it actually was.
git checkout -b copilot-challenge-submission
git push origin copilot-challenge-submission
Two commands. Branch created, pushed to GitHub.
The thing I had been afraid of for months took about 90 seconds.
This is one of those lessons that only clicks when you have a real reason
to do it. The challenge forced my hand and I am genuinely grateful for that.
I now understand branching, I understand why teams use it, and I understand
how to merge back to main when the work is done. A wall I had been walking
around for months turned out to be a door I just hadn't tried to open.
I kept the original broken code on main intentionally β as honest
documentation of where I started. The entire transformation lives on
copilot-challenge-submission. Anyone can compare the two branches on GitHub
and see exactly what changed.
Phase 2: Setting Up the Right Way
The challenge required work on a dedicated branch, which aligned perfectly with good Git hygiene. I created copilot-challenge-submission from main to keep the original skeleton untouched and build the finished version on top.

Branch created, Copilot sidebar active in VS Code. Ready to go.
One important early step: adding ADMIN_SECRET to the .env file. The original codebase accepted a role field on registration with zero protection β anyone could register as "admin" by just passing "role": "admin" in the body. Copilot helped me add a secret-key guard:
// anyone can register, but claiming admin requires the secret key
let assignedRole = "member";
if (role === "admin" || role === "project_admin") {
if (adminSecret !== process.env.ADMIN_SECRET) {
throw new ApiError(403, "Invalid admin secret key");
}
assignedRole = role;
}
Small change. Massive security difference.
Phase 3: Fixing the Bugs with Copilot
I opened each broken file and used Copilot Chat (sidebar) to describe what I was seeing. The workflow was:
- Open the file in VS Code
- Describe the symptom to Copilot: "This function always returns 'token invalid' even for fresh tokens β why?"
- Copilot would identify the issue and suggest the fix
- I reviewed, understood, and accepted

Copilot identifying the sha-256 hash algorithm bug. The fix is one character β removing the hyphen β but finding it without AI would have taken much longer.
Here is the full bug list, fixed in one focused session:
| # | Bug | File | Impact |
|---|---|---|---|
| 1 |
sha-256 β sha256 in crypto hash |
auth.controllers.js |
Password reset always failed |
| 2 |
forgotPasswordMailgenContent not imported |
auth.controllers.js |
ReferenceError in production |
| 3 |
action and outro outside body in email template |
mail.js |
Forgot-password email had no button |
| 4 | HTTP status 489 (not real) |
auth.controllers.js |
Invalid response code |
| 5 | Login only searched by email, ignored username | auth.controllers.js |
Username login silently failed |
| 6 | Route typo /resend-emil-verification
|
auth.routes.js |
Endpoint unreachable |
| 7 | Dead { header } import |
auth.middleware.js |
Lint noise, dead code |
| 8 | Verify email URL had /users/ not /auth/
|
auth.controllers.js |
Every verification email 404'd |
After fixing all 8: npm run dev β register β check Mailtrap β click verify link β 200 OK. First time that flow had ever actually worked end to end.
A Note on Honesty: When Copilot Wasn't Enough
GitHub Copilot was my primary tool throughout this sprint β
but I want to be transparent about something.
Copilot has usage limits. There were moments, especially during
the longer building sessions on Day 3 and Day 4, where I hit
those limits mid-flow. A schema half-written. A controller
function halfway through. The suggestion stream would slow down
or stop responding the way it had been.
In those moments, I did what any developer would do β
I used what was available. I turned to other LLMs (Claude and
ChatGPT at different points) to keep the momentum going,
asked similar questions, got the code, reviewed it the same way
I reviewed Copilot's output, and kept building.
I am mentioning this because I think honesty matters more than
a clean narrative. The challenge is called a "Finish-Up-A-Thon"
β the goal is to finish the project. The AI tools I used were
assistants, not authors. Every line of generated code went
through my eyes, my understanding, and my decision to accept,
modify, or reject it.
What I can say with confidence: GitHub Copilot inside VS Code β
the inline suggestions, the Chat sidebar, the Ctrl+I inline
chat β handled the majority of the heavy lifting.
The workflow of describing what I wanted in plain English and
getting working code back in seconds is genuinely transformative
for a developer at my stage.
The bug-finding session on Day 1 was almost entirely Copilot.
The activity logger pattern β Copilot. The RBAC middleware β
Copilot. The Swagger JSDoc annotations across 40+ routes β
Copilot with some Claude assistance when the limit hit.
I learned from all of it. That is what matters.
Phase 4: Building What Was Always Missing
With a stable auth foundation, I shifted to building the actual project management system. This is where Copilot went from debugging tool to genuine pair programmer.
The Data Models
I navigated to src/models/ and used Copilot Inline Chat (Ctrl+I) to scaffold each new schema. My prompt style was always specific about relationships:
"Create a Mongoose schema for a Project model. It should have name, description, status (enum: active/on_hold/completed/cancelled), a createdBy ObjectId ref to User, and a members array where each member has a user ObjectId ref and a role string. Add compound indexes for createdBy and members.user."
Four new models created:
src/models/
βββ project.models.js β Project with embedded members[]
βββ task.models.js β Updated: added priority, dueDate, project ref
βββ comment.models.js β Comments on tasks
βββ activity.models.js β Audit log for every action
The Activity Logger β My Favourite Part
The most elegant piece of the system is the logActivity() utility. It gets called after every meaningful action across every controller β but it's designed to never crash the main request even if it fails:
export const logActivity = async (action, entity, entityId, userId, metadata = {}) => {
try {
await ActivityLog.create({ action, entity, entityId, performedBy: userId, metadata });
} catch (err) {
// Silently swallow β logging must never break a real request
console.error("Activity log failed silently:", err.message);
}
};
Now every create, update, delete, login, and comment is recorded. Admins can query GET /api/v1/activity and see a full audit trail. Members see only their own activity.
I described this pattern to Copilot and it immediately suggested the try/catch wrapper with the silent swallow β a pattern I had read about but never implemented myself.
Task Management: More Than a Todo List
The original constants.js had TaskStatusEnum defined (todo, In_progress, done) but no task model, no routes, and no controllers. The constants were written but the feature was never built.
I added TaskPriorityEnum to match:
export const TaskPriorityEnum = {
LOW: "low",
MEDIUM: "medium",
HIGH: "high",
URGENT: "urgent",
};
Then built the full task system on top β with one GET /tasks endpoint that does everything:
GET /api/v1/tasks?search=login&priority=urgent&overdue=true&sortBy=dueDate&order=asc&page=1&limit=10
That single endpoint handles full-text search across title and description, filter by status/priority/assignee/project, overdue detection, sorting, and pagination β all composable together.
The Tool I Had Never Seen Before: Swagger
I want to be upfront about something β before this challenge,
I had never used Swagger in my life.
I had heard the word. I had seen screenshots of it in tutorials.
But I had never actually sat down, configured it, and had it
generate live documentation from my own code.
When I first ran npm run dev after wiring up swagger-ui-express
and opened http://localhost:8000/api/v1/docs β I genuinely did
not expect what I saw. Every single route, laid out visually.
Request bodies with example values. A padlock icon showing which
routes needed authentication. A "Try it out" button that let me
test my own API without opening Postman.
I spent probably 20 minutes just clicking through it before I
remembered I had more features to build.

My first time seeing Swagger UI on my own project.
Every route documented, explorable directly in the browser.
The learning curve was real though. My first Swagger setup
showed "No parameters" on every POST route β because I had
forgotten that Swagger needs JSDoc @swagger comments above
each route to know what the request body looks like.
The routes existed and worked perfectly, but Swagger had no
idea what data they expected.
Here is what an empty POST route looks like in Swagger vs
a documented one:
// β Before β Swagger shows "No parameters"
router.route("/projects").post(createProject);
// β
After β Swagger shows a full interactive form
/**
* @swagger
* /api/v1/projects:
* post:
* summary: Create a new project
* tags: [Projects]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* properties:
* name:
* type: string
* example: My Awesome App
* status:
* type: string
* enum: [active, on_hold, completed, cancelled]
* example: active
*/
router.route("/projects").post(createProject);
Once I understood the pattern, documenting every route became
satisfying rather than tedious. You write the comment once,
and Swagger generates a fully interactive UI from it.
Any developer who clones the repo can open /api/v1/docs,
authorize with their JWT token, and test every single endpoint
without reading a single line of code.
That is what "production-grade API documentation" actually means.
I understood the concept before this project.
Now I understand why it matters.

POST /tasks with all fields filled β title, priority urgent,
due date set. One click to Execute. This is what
"Try it out" looks like in practice.
Phase 5: The Features That Make It Shine
Projects with Member Access Control
POST /api/v1/projects β create (creator auto-becomes project_admin)
GET /api/v1/projects β list projects I created + am member of
GET /api/v1/projects/:id β project detail + all its tasks
PATCH /api/v1/projects/:id β update (project_admin or owner)
DELETE /api/v1/projects/:id β delete (owner only, unlinks tasks)
POST /api/v1/projects/:id/members β add member with role
DELETE /api/v1/projects/:id/members/:userId β remove member
Non-members get a clean 403 when trying to access a project they don't belong to. The PROJECT_ADMIN role that was defined in the original constants.js but never enforced now actually does something.
Task Comments
Three routes, one model, makes the whole system feel collaborative:
POST /api/v1/tasks/:taskId/comments β add comment
GET /api/v1/tasks/:taskId/comments β paginated list
DELETE /api/v1/tasks/:taskId/comments/:id β delete own comment (or admin)
Deleting a task cascade-deletes all its comments. getTaskById now includes a commentsCount field.
Dashboard Stats
GET /api/v1/tasks/stats
Returns:
{
"stats": {
"total": 24,
"byStatus": { "todo": 10, "In_progress": 9, "done": 5 },
"byPriority": { "urgent": 3, "high": 7, "medium": 11, "low": 3 },
"myTasks": 6,
"overdueTasks": 2,
"recentTasks": [...]
}
}
One endpoint. Everything a frontend dashboard needs.
The Final API Surface
Auth β /api/v1/auth
| Method | Endpoint | Auth |
|---|---|---|
| POST | /register |
β |
| POST | /login |
β |
| POST | /logout |
JWT |
| GET | /current-user |
JWT |
| GET | /verify-email/:token |
β |
| POST | /resend-email-verification |
JWT |
| POST | /refresh-token |
β |
| POST | /forgot-password |
β |
| POST | /reset-password/:token |
β |
| POST | /change-password |
JWT |
| PATCH | /update-avatar |
JWT |
| PATCH | /update-profile |
JWT |
Tasks β /api/v1/tasks Β· Projects β /api/v1/projects Β· Activity β /api/v1/activity
Production Signals Added
| Signal | Implementation |
|---|---|
| Security headers |
helmet() β 11 headers set automatically |
| Rate limiting |
express-rate-limit β 10 req/15 min on auth endpoints |
| Request logging |
morgan β dev mode and combined production format |
| Global error handler | 4-param Express middleware β no stack traces to clients |
| 404 handler | Custom JSON response for unknown routes |
| Input validation |
express-validator on all auth routes |
| File upload validation | multer with type filter (JPEG/PNG/WebP) + 2MB limit |
| API documentation | Swagger UI + swagger-jsdoc, OpenAPI 3.0 |
| Audit logging | Every meaningful action logged with metadata |
| RBAC |
verifyRole() middleware enforced at route level |
Postman Proof

User registration returning 201 with the created user object (sensitive fields excluded).

User registration For Admin user returning 201 with the created user object (sensitive fields excluded).

Login returning access token, refresh token, and user object. Tokens auto-saved to Postman environment via test script.

Dashboard stats endpoint β aggregated counts by status and priority.

Activity feed showing audit trail of all actions.

RBAC working correctly β member role correctly blocked from deleting tasks.
What GitHub Copilot Actually Did
I want to be specific because "I used Copilot" is easy to say.
Copilot found bugs I would have stared at for hours. The sha-256 vs sha256 issue β I had been running the reset flow and getting "token expired" responses. I described the symptom to Copilot Chat and it immediately asked "is the hash algorithm name correct in Node.js crypto?" β and that was it. Five seconds.
Copilot taught me patterns I knew existed but hadn't implemented. The silent-swallow try/catch in logActivity(). The $or MongoDB query for login by email or username. The validateBeforeSave: false pattern in Mongoose saves. I knew all of these things conceptually. Copilot showed me the exact idiomatic way to write them.
Copilot accelerated schema and boilerplate generation. Every new model, every new controller β I described what I wanted in plain English and got working code back in seconds. I reviewed every suggestion before accepting. I rejected probably 20% and adjusted another 30%. But starting from something working is dramatically faster than starting from blank.
Copilot didn't write the architecture. The decision to use an embedded members[] array in Project rather than a separate collection β that was mine. The fire-and-forget logger pattern β I described it to Copilot and it implemented it. The overdue filter composing with other query params β my design, Copilot's implementation. This felt like genuine pair programming.
What I Learned
Finishing is harder than starting. Starting a project is exciting β you make fast decisions and the wins come quickly. Finishing means auditing what you have, being honest about what's broken, and fixing unglamorous bugs before adding new features. The 8-bug fix session on Day 1 was the most important thing I did.
The "silent failure" is the worst kind of bug. Six of my eight bugs produced no error β they just returned wrong data or hit a route that didn't exist. Without end-to-end testing, you'd never find them. With Copilot, describing the symptom was enough to surface the cause.
AI pair programming works best when you lead. The best outputs came when I was specific: "This function needs to search by email OR username using MongoDB's \$or operator β modify the findOne call" produced better results than "fix my login function." Copilot responds to context and intent. The more precisely I described what I wanted, the less reviewing and editing I had to do.
Git branching went from intimidating to obvious. I had avoided branches
for months because the terminology looked complex. The challenge requirement
forced me to actually do it β and it took two commands. Sometimes the only
way to stop fearing a tool is to have no choice but to use it.
Links
- GitHub Repo: github.com/AliHaroon111/Project-Management-Backend
-
Branch:
copilot-challenge-submission -
API Docs (local):
http://localhost:3000/api/v1/docsafternpm run dev


Top comments (11)
Wow π², thank you so much for the detailed feedback, Abdul Azeem! I really appreciate you pointing out those specific sections.
I felt it was crucial to include the moments where AI hit its limits because that is the reality of day-to-day engineering with these tools. Copilot is an incredible co-pilot, but the developer still has to navigate and lead the path.
The
sha-256hyphen trap was definitely a wake-up call on how silent failures can drain your hoursπ₯Ί. And honestly, putting my initial anxiety about Git branching out there felt risky, but I'm so glad it resonated with you. Breaking through that main-only habit was one of my favorite wins of this challenge! Thank you π for the support!The "broken auth template" starting point is the universal experience, and it's the perfect illustration of why auth is the boring-but-critical part everyone underestimates. Templates get you 70% of an auth flow and then leave the dangerous 30% (token refresh, session invalidation, password reset edge cases, role checks on every endpoint, the security holes that don't show up until someone probes them) as an exercise for the reader. Taking it from "template that compiles" to "production-grade" means closing exactly those gaps, and Copilot is genuinely useful there as long as you verify what it generates against the real security requirements rather than trusting it - because an AI will happily write auth code that looks right and leaks.
That's the exact gap I work on - auth/billing/deploy is the 20% that's invisible in a demo and fatal in prod, and it's the whole reason Moonshift exists (the thing I build): a multi-agent pipeline that takes a prompt to a deployed SaaS on your own GitHub + Vercel, with the boring-critical parts wired as verified defaults instead of a half-finished template. A verify layer checks the generated code so "looks right" has to actually be right. Multi-model routing keeps a build ~$3 flat, first run free no card. Nice turnaround on this. What was the worst gap the template left in the auth - token/session handling, or missing authorization checks on endpoints? Those two cause the most real incidents.
Thank you, Harjot! π Your breakdown of that "invisible 20%" is spot on. AI writing code that "looks right but leaks" is exactly why we developers still need to keep our eyes wide open! π― Moonshift sounds like an amazing tool for solving this exact headache.
To answer your question: hands down, the worst gap the original template left was missing authorization checks on the endpoints, even more than the token handling bugs! π οΈ
Here is what made it a real hazard:
ADMIN_SECRETguard key check so nobody could fake an admin account, the individual routes themselves were still wide open.While the broken token lookup hash (
sha-256vssha256) completely bricked the password reset flow for users, a lack of endpoint authorization means your data is exposed to malicious requests.Writing that strict
verifyRole()middleware to actively block non-admins on a route-by-route level was the most critical "production-grade" shift we made! πͺThanks for raising such a great real-world question! π€
Great write-up, and congrats on knocking out those 8 silent bugs! The transition from sha-256 to sha256 is such a classic Node.js crypto trap that is easy to miss without a second pair of eyes.
βI wanted to ask about the ADMIN_SECRET fix you implemented in Phase 2. While it definitely prevents unauthorized admin registration compared to the initial broken code, hardcoded secrets or environment-variable checks for roles can sometimes become tricky to scale as a team grows. If an admin secret leaks, rotating it might break existing admin flows or require updating multiple clients. Did you and Copilot consider using a proper Role-Based Access Control (RBAC) table or an initial setup seed script instead, or did you stick with the .env secret approach specifically to keep the boilerplate migration lightweight for this challenge?
Spot on, Muhammed! That is an excellent architectural observation.
You are completely righ π the
.envvariable approach forADMIN_SECRETwas a strategic choice to keep this specific migration lightweight, straightforward, and secure against anonymous payload manipulation right out of the gate.During the development sprint, Copilot and I actually discussed expanding this into a database-driven seed architecture or dedicated RBAC table schema. As a team grows, secret key rotation would definitely become a bottleneck. For production scaling, migrating this role verification to database-backed permissions inside the
verifyRolemiddleware is definitely the next logical evolutionary step for this codebase. Thanks for raising such a solid production-grade point!Your article raises a very important point about the future of AI and secure backend systems. But I want to ask: how can developers in countries like Pakistan practically compete with large companies when training, hardware, and deployment costs are still very high, even with open-weight models like Gemma 4?
Thank you, Anas! This is a highly realistic question that hits the core of the infrastructure bottleneck.
While training massive base models from scratch is out of reach for independent developers locally, the competitive edge for us lies in optimization and fine-tuning (using techniques like LoRA/QLoRA) or heavy orchestration via RAG (Retrieval-Augmented Generation).
Open-weight models like Gemma 4 drastically shift the economics π because we don't pay per-token API taxes to third-party providers in USD. By hosting quantized versions on highly affordable local infrastructure, or running local-first development setups
we can build specialized, secure apps tailored to our local market with near-zero ongoing API runtime costs. The battle isnt about raw compute
it's about context engineering and domain-specific implementation!
Really impressive transformation. Taking a broken auth template and turning it into a production-grade project management API shows real growth, especially the way you handled silent bugs, RBAC, activity logging, Swagger documentation, and Git branching. I also appreciate the honesty about using AI as a pair programmer, not as a replacement for understanding. This is exactly how developers should use tools like GitHub Copilot: lead with clear thinking, review every suggestion, and ship something solid. Great work and best of luck for the challenge!
Thank you so much, Irfan! Your comment perfectly summarizes what I hoped to demonstrate with this project.
The goal was to treat AI as a true pair programmerβusing it to fast-track boilerplate schema design, unearth edge-case runtime bugs and write exhaustive Swagger annotations, while keeping my hands firmly on the architectural steering wheel (like designing the activity telemetry and project ownership flows).
It was an intensive 4 days of building, but seeing everything tie together with a clean API documentation layer made the whole sprint incredibly rewarding. I appreciate π the encouragement!
Some comments may only be visible to logged-in visitors. Sign in to view all comments.