DEV Community

Cover image for The Login That Never Worked: Debugging 5 Layers Deep
Dinesh Wijethunga
Dinesh Wijethunga

Posted on Originally published at dineshstack.com

The Login That Never Worked: Debugging 5 Layers Deep

Part 5, the finale of a 5-part series on using Claude AI to run, secure, and ship a real production server. The pipeline shipped (Part 4). Then I tried to log in.

The Login That Never Worked — Debugging Five Layers Deep with AI (Part 5)

The pipeline was green. Both releases were live. I opened my SaaS to see it working — and got a 502. Then, after fixing that, a "CORS error." Then a 500. Then an OAuth failure. Then a 403. Each fix revealed the next problem underneath, like peeling an onion that makes you cry five times.

This is the most instructive post in the series, because it shows what AI debugging actually looks like on a real system: not one magic answer, but a disciplined peeling of layers, where each error's real cause is hidden behind a misleading symptom. Here's all five, in order.

Layer 1: The 502 — A Stopped Process

The frontend returned 502 Bad Gateway. nginx proxies the site to a Node process on port 3003, and Claude's first check — pm2 list — showed the visa-saas process was stopped. The deploy's process-reload had left it in a bad state.

The fix wasn't a blind restart (that can hide a crash loop). Claude had me delete the corrupted process entry and start it fresh:

pm2 delete visa-saas
pm2 start ecosystem.config.js
pm2 save   # so it survives reboots

Process online, port 3003 answering. Lesson learned: the deploy's "reload" logic couldn't recover a stopped process — a real pipeline bug we noted for a follow-up fix. A green deploy that leaves the site down is worse than one that fails honestly.

Layer 2: The "CORS Error" That Wasn't CORS

Now the site loaded but login threw what the browser called a CORS error. Here's the trap Claude flagged that saves hours: when a Laravel API throws a 500, the error response often has no CORS headers — so the browser reports "CORS policy" when the real problem is the API crashing. The CORS message was a symptom, not the disease.

And the disease connected back to our Part 3 work. Remember the storage-permissions fix? On this new release, the storage folder was owned by the deploy user, but PHP-FPM runs as the web user — so PHP couldn't write logs or sessions, threw 500s, and the browser painted them as CORS. Claude spotted it because it read the actual error, not the browser's guess. One chown and the 500s vanished.

Layer 3: The OAuth Failure — A Wrong Client ID

Login now reached the API and returned a precise JSON error (progress — a clean error means CORS is genuinely fine):

"Client authentication failed"
League\OAuth2\Server\Exception\OAuthServerException

Laravel Passport couldn't issue a token. Claude traced it into the database. There were two OAuth clients:

ID Client Type
1 Password Grant Client personal = 0
2 Personal Access Client personal = 1

But the app's config pointed PASSPORT_PERSONAL_ACCESS_CLIENT_ID=1 — the wrong type. It was trying to issue a personal access token using a password-grant client. Changing the ID to 2 and rebuilding the config cache fixed it. Almost certainly a bug that had existed since setup — because with login broken, nobody had ever successfully authenticated on this deployment. Our work didn't break it; it was the first time anyone got far enough to hit it.

Layer 4: The Permission-Write 500s

Logged in! Then the dashboard threw 500s on loading modules. Same root cause as Layer 2, different file: the app writes a modules_statuses.json at runtime, owned by the wrong user, so the web process couldn't write it. Another targeted chown. This was the moment the deeper pattern became clear: the deploy creates files as one user, the app runs as another — the real fix is a permission-reconciliation step in the pipeline itself, so every future deploy doesn't reintroduce it.

Layer 5: The 403 — A One-Word Typo

The final boss. Every data endpoint returned 403 Forbidden — "You do not have the required permissions." But I was logged in as a super-admin. Claude checked the database: my user had the super-admin role, the role had all 60 permissions, everything linked correctly. The data was perfect. So why 403?

This is where I pointed Claude Code at the actual codebase, and it traced the exact chain. The route guarding /users required a permission named manage-users. But the seeder that creates permissions created users.viewthe permission name the route demanded had never been created. A single naming mismatch between the route and the seeder. Every user in the system was locked out of that endpoint, and had been forever.

Route wants:    permission:manage-users   ← doesn't exist
Seeder creates: users.view                ← the real name

The fix: change the route guard to the permission that actually exists. And because AI is good at exactly this kind of grind, I had it grep the entire route file for other mismatches — so we could fix them all in one pass instead of discovering them one 403 at a time.

Why This Story Matters More Than a Clean Success

Five layers. A blank page that was a stopped process. A CORS error that was a permissions problem. An OAuth failure that was a config typo. A 403 that was a naming mismatch. Not one of these symptoms pointed at its own cause. That's what real debugging is — and it's exactly where an AI partner earns its place: reading the actual error instead of the misleading one, tracing a symptom to its true source, and grepping a codebase for every instance of a bug in seconds.

The AI didn't replace my judgment — I decided what to fix by hand, what to route through the pipeline, and when to stop and think. But it turned a bewildering cascade into a solvable sequence. And critically: bugs 3, 4, and 5 were pre-existing — they'd been in that codebase since setup, invisible because nobody could log in to trigger them. We didn't cause them. We were the first to reach them, and the AI helped root-cause each one instead of flailing.

The Whole Journey, Start to Finish

Over one day, with Claude AI as an investigate-and-verify partner, this server went from: an internet-exposed financial API, world-readable secrets across 20 projects, password-based SSH open to the world, no deployment pipeline, and a frontend that had never worked — to: a locked-down box, key-only SSH, a zero-downtime CI/CD pipeline, and a working login on a live dashboard.

Key Takeaways for New Developers

  • The symptom is almost never the cause. A 502, a CORS error, and a 403 all lied about what was really wrong.
  • Read the actual error, not the browser's interpretation of it.
  • AI is exceptional at tracing a symptom to its source and grepping a whole codebase for every instance of a bug.
  • Keep the human in charge of what to fix and where it belongs (hand-fix vs. pipeline); let the AI do the tracing and the grind.
  • A working system revealing old bugs isn't a regression — it's progress. You can only find the login bug once login gets far enough to fail.

That's the series. One developer, one AI partner, one very real production server — audited, secured, shipped, and debugged, the honest way, with a paper trail at every step. If you're nervous about letting AI near your infrastructure, I hope this showed you the pattern that makes it not just safe but genuinely powerful: the AI gets the brains, you keep the keys.

Thanks for reading all five parts. If this helped, the whole series is built to be followed step by step on your own server. What would you point an AI agent at first — a security audit, or that one bug you've been avoiding?


Originally published at dineshstack.com — read the full version with code samples and updates there.

Top comments (0)