19 Bugs in 2 Hours: What Happens When You Actually Try to Use Your AI-Built Enterprise App
By Pushpak Pujari ·
February 14, 2026 ·
11 min read
I declared victory twice. Both times I was wrong.
Let me tell you about the worst two hours of my recent engineering life.
We'd just finished an autonomous build loop — 85 engineering tasks, executed by Claude Code over ~16 hours, building a full enterprise Contract Lifecycle Management (CLM) application. NestJS backend with Prisma, Next.js frontend, multi-tenancy, RBAC, the works.
The build loop reported success. TypeScript compiled. Next.js built. Tests passed. I messaged the team: "CLM MVP is done."
Then I opened a browser.
2:15 PM — The App Doesn't Exist
I hit localhost:3000. Blank page. No login screen. No error message. Just... nothing.
Bug #1: The build loop had created 80+ feature pages (contracts, templates, clauses, approvals) but never created the scaffolding. No root layout.tsx. No auth layout. No dashboard layout. No login page. Next.js App Router requires layout.tsx at each route group level. This should have been Task #1, not something buried in phase 3.
The autonomous loop built an entire house — bedrooms, kitchen, bathrooms — but forgot the front door.
I created four layout files and a login page from scratch. Thirty minutes in, zero features tested.
2:20 PM — "Tenant ID Required"
Login page rendered. Entered credentials. Hit Sign In.
Bug #2: "Tenant ID is required." Every API call needed an x-tenant-id header. The build loop had implemented multi-tenancy perfectly — for a production environment with a tenant provisioning flow. For a dev environment where I just want to log in? Completely unusable.
I added a DEFAULT_TENANT_ID env var and middleware bypass. Multi-tenancy middleware must have a dev-mode escape hatch. The AI built for the spec, not for the human trying to use it.
2:30 PM — Login Works, Everything Else Crashes
JWT came back. Dashboard loaded. Then immediately crashed.
Bug #3: Three compounding auth failures. The JWT strategy didn't load user permissions. The permissions query used wrong Prisma relation names. The JWT user object was missing the id field entirely. DataAccessService expected req.user.id and got undefined.
Auth is a chain — JWT creation → validation → user hydration → permission loading → route guards. The build loop built each link in isolation. Nobody tested the chain end-to-end. Every individual piece worked. Together, they crashed.
2:45 PM — Prisma's Silent Massacre
Bug #4: Various API endpoints returned 500 errors on any query involving joins. The multi-tenancy middleware auto-injected WHERE tenantId = ? into ALL Prisma queries. But models like ContractRelationship, ContractShare, DocumentContent, and SignatureRequest don't have a tenantId field.
I had to add 15 models to the exclusion list. The build loop created new models without knowing to update the middleware's exclusion list. Auto-injection middleware is a landmine if your exclusions aren't maintained.
2:50 PM — The ORM vs SQL Identity Crisis
Bug #5: Search endpoint — 500 error. The raw SQL queries used Prisma model names ("Contract", "Template") instead of PostgreSQL table names ("contracts", "templates"). $queryRaw bypasses the ORM. Prisma model names don't apply.
Bug #6: Same endpoint, second error. LIMIT $N with a string parameter. PostgreSQL wanted an integer. Inline LIMIT ${Number(limit)} fixed it.
Two bugs in the same file. One conceptual (ORM vs SQL naming), one trivial (type casting). Both invisible until runtime.
2:55 PM — The Phantom Relation
Bug #7: Audit logs endpoint — 500 error. The service included { user: { select: { id, name, email } } } but the AuditLog model has no user relation in the Prisma schema.
Here's the insidious part: Prisma compiles fine with non-existent includes. TypeScript didn't catch it. The build didn't catch it. Only a real API call at runtime surfaced the error.
3:00 PM — The Permissions Gap
Bug #8: Reports endpoint — 403 Forbidden. Even for Admin users. The seed script simply didn't include report:read and report:export permissions. The build loop created the reports feature, created the RBAC guards, but never seeded the permissions those guards check.
3:13 PM — The Method That Never Was
Bug #9: TypeScript caught this one — ObligationService.listAll() was referenced but never implemented. A method signature existed, calling code existed, but the body? Missing. The build loop wrote the consumer before the provider.
3:30 PM — Three Pages That Don't Exist
Bug #10: After fixing all the API issues, I finally opened a browser and clicked through the sidebar. Contracts? Blank. Search? Error. Admin? Nothing.
The build loop created API endpoints and some frontend components, but never created the actual page.tsx files for three major routes. I wrote 665 lines of React across three new pages. From scratch. In a "completed" application.
3:35 PM — The URL That Went Nowhere
Bug #11: Contracts page rendered beautifully — with zero data. The API client used /contracts instead of /api/contracts. The NestJS app mounts everything under /api/. One missing prefix, complete data invisibility.
Bug #12: Templates loaded but showed duplicates. The apiClient.get() already unwraps the {data} envelope. templatesApi.list() called .data again. Double-unwrap → empty or duplicated results.
3:50 PM — The Parameter Name Drift
Bug #13: Templates page — "Failed to load templates" toast. Frontend sent ?sort=name. Backend DTO expected sortBy. Validation rejected the unknown property.
Frontend and backend were built by different tasks, 30 tasks apart, with no shared schema. Query parameter names drifted. This is the speculative code problem in a nutshell — the frontend guessed what the backend would accept, and guessed wrong.
3:53 PM — The Auth Race Condition
Bug #14: This was the sneaky one. Clauses, Reports, and Approvals pages all returned 401 Unauthorized. But Dashboard and Contracts worked fine.
The token getter was wired via apiClient.setTokenGetter() in the zustand store's setToken() method. On page navigation, some pages fired API calls before the store rehydrated and wired the getter. Dashboard worked because its loading pattern was slightly different.
Fix: Three-layer defense. Token getter function (primary), localStorage.getItem('auth-storage') fallback (reads zustand's persisted state directly), explicit token parameter override. Ugly. Bulletproof.
Bug #15: Same moment. Clauses page — Cannot read properties of undefined (reading 'length'). The 401 from Bug #14 caused the API call to throw. The catch block showed a toast but didn't set clauses to []. Component accessed clauses.length on undefined.
Bug #16: Reports page — "No reports found" despite the API having 10 reports. Same 401, plus the API double-wraps: {data: {data: [...]}}. Two bugs overlapping.
4:21 PM — The Final Three
Bug #17: Audit log page crashes on log.userId.substring(0, 8). Some audit logs are system-generated — userId is null. One missing ?. operator.
Bug #18: No visible logout button. Once logged in, you're trapped. The sign-out button existed in the sidebar code but was clipped or never rendered.
Bug #19: The admin hub linked to 6 sub-pages. Only 2 had page.tsx files. The other 4 returned 404.
Then Came Round 2
After fixing all 19 bugs, I ran the test suite: 30/30 API tests, 38/38 frontend tests, 3/3 Playwright E2E. All green. I declared victory again.
Then I clicked into a detail page.
Every single one crashed.
- Templates:
section.type.replace('_', ' ') — sections don't have a type field. They have title and clauses[].
- Clauses: TipTap editor crashed without
immediatelyRender: false in Next.js SSR. One-line fix.
in isolation. The seams between pieces is where everything fell apart.
"It builds" ≠ "it works." Tests passing ≠ app working. The build loop validated with tsc --noEmit and next build. Those catch type errors and import issues. They don't catch wrong URLs, auth race conditions, response format mismatches, or missing database seeds.
The 60/40 Rule
85 tasks built in 3 hours. 27 bugs took 4.5 hours to fix. If you're planning an autonomous build, budget 60% for building, 40% for integration and verification. Not because the AI writes bad code — each individual task was solid. Because integration testing is where the real work lives.
This Is Why We Build Vizops
This experience is the entire thesis behind Vizops. AI can generate code at incredible speed — Day 1 is genuinely magical. But Day 2? Day 2 is 27 bugs, 4.5 hours, and the slow realization that "it compiles" means almost nothing.
The gap between demo and production is where enterprises get stuck. Not because the AI isn't smart enough to write the code. Because nobody's optimizing for the system actually working — end-to-end, in a browser, with real data, under real conditions.
That's the problem we're solving. Not just building agents, but making them actually work. Continuously. Reliably. In production.
Written by someone who declared victory twice before actually opening a browser. Don't be that person. Always click through the UI.*