// case study

Same UI, production-grade underneath

The companion piece to BookBase. Different platform, different playbook: Lovable lets you export the code, so the rescue is a refactor in place rather than a rebuild from scratch.

scope In-place refactor of a Lovable-generated invoicing app
stack Next.js · TypeScript · Supabase · Vercel
time ~1 week of senior engineering
// the short version

Same UI on top. Different posture underneath.

InvoiceFlow is a small invoicing SaaS: dashboard, invoices, clients, basic filtering. We built it in Lovable in about an hour, then handed the codebase to ourselves as if a client had just sent it over asking for a production audit.

We worked through the codebase end to end and fixed every issue we found. Ten of them are documented below; they range from "your credentials are one commit away from being public" to "your dashboard chart loops over every invoice twelve times on every render." The after-version is running in Vercel and Supabase. The UI looks nearly identical to the before-version. Everything underneath is different.

This is the companion piece to our BookBase case study, which covers the same pattern on base44. The headline difference: Lovable lets you export your code. That changes the whole playbook. Instead of rebuilding from scratch, we kept the repo and transformed it in place.

Same UI, production-grade underneath screenshot
// why this case looks different

Two kinds of rescue, depending on whether you own the code.

Vibe-coded rescues split into two engagement types depending on whether you can export.

If you cannot export (base44, Bubble, some no-code platforms), there is nothing to refactor. The app is a rental. The only rescue path is a rebuild on a stack you actually own. That was BookBase.

If you can export (Lovable, Bolt, Cursor, v0, Replit Agent), you have a Git repo, a package.json, the actual source. The right move is almost always to keep it, audit it, transform it, harden it. That is this one.

For InvoiceFlow, the workflow was: clone the Lovable repo, run our production-ready transformation prompt as a first pass, audit what the AI fixed versus what it missed, then do the real engineering work on top. About a week of senior time, end to end.

// the issues, by category

Ten representative fixes, grouped by what they cost you.

A cross-section of the work: the security holes that ruin a week, the architecture choices that compound, and the production gaps that make every other problem invisible.

[01]

The .env file was not in .gitignore.

One careless commit away from your Supabase service role key being public on GitHub. The first thing a motivated attacker does is scan repos for leaked credentials.

[02]

Database queries did not filter by user_id.

Every SELECT relied on Supabase RLS for isolation. RLS is good. A well-designed system enforces tenant boundaries at both layers, query and policy, so a misconfigured policy cannot leak data across tenants.

[03]

TypeScript strict mode was off.

The default Lovable tsconfig had strict disabled. Half the point of using TypeScript is strict mode. Without it, the compiler shrugs at the bugs it is supposed to catch.

[04]

Five "as any" casts scattered across the codebase.

Each one is a silent bet that the shape of some runtime value is what you think it is. That bet loses eventually.

[05]

Zero error boundaries.

A single thrown exception anywhere in the component tree crashes the entire app. No fallback UI, no recovery path, no monitoring to tell you it happened.

[06]

window.location.href used instead of React Router.

Navigation triggered full page reloads, losing all client-side state. In a single-page app, this is the equivalent of slamming the door every time you walk into a room.

[07]

Mutations had no loading state.

The Send Invoice and Save as Draft buttons stayed active during submission. Five separate buttons had this problem. A user clicking twice could create duplicate invoices.

[08]

Form validation only checked for empty descriptions.

Negative unit prices, accepted. Missing client, accepted. Due date before issue date, accepted. Quantity of -1,000, accepted, and the total just went negative.

[09]

Dashboard chart looped over all invoices twelve times.

The chart needed monthly totals for the past twelve months, so the code looped through the full invoice list once per month. O(12 × n) where O(n) is correct. Invisible at 50 invoices, very visible at 5,000.

[10]

Invoice numbers generated in the browser.

Each new invoice got a number like INV-4955 computed client-side by adding 1 to the last one the user had seen. Two users submitting at the same time get the same number. Concurrent submissions silently collide.

AI is pattern-matching to the simplest working implementation, not the correct one. The work is telling the difference.

// what we changed

Same repo, same UI, four lanes of work.

Because Lovable exported the code, this was an in-place transformation rather than a rebuild. About a week of senior engineering, distributed across the lanes below.

// security

Security & data

  • .env hardened, secrets rotated
  • All queries scoped by user_id / org_id
  • RLS policies audited and tightened
  • Penetration test against weakened RLS
// code-health

Code health

  • TypeScript strict mode on, 0 errors
  • All "as any" casts removed
  • StatusBadge consolidated to one component
  • Page components split, concerns separated
// reliability

Reliability & observability

  • Sentry + Slack alert routing
  • Vercel Analytics + uptime checks
  • Error boundary on the app shell
  • Console logs stripped from production
Same UI, production-grade underneath screenshot
// before / after

A line-by-line receipt, not a vague hardening claim.

Every row is a specific, independently verifiable fix. This is the shape of a real production audit output: not "we made it better," but a list you can hand to your lead engineer or your auditor.

Metric
Before (Lovable)
After (refactor)
TypeScript strict mode
Off
On, 0 errors
"as any" casts
2
0
"err: any" casts
3
0
Unscoped SELECT queries
9
0
Duplicated StatusBadge copies
4
1 (shared)
Error boundaries
0
1
Console logs in production
1
0
window.location hacks
1
0
Buttons without loading states
5
0
Form validation checks
1
5
Dashboard chart complexity
O(12 × n)
O(n)
.env protected
No
Yes
Error monitoring
None
Sentry + Slack routing
Uptime monitoring
None
Vercel + external checks
Test coverage
0
Unit + E2E on critical flows
Invoice number collisions
Possible
Prevented (DB sequence)
// the four that matter most

If you only had time to fix four things.

Not all of the fixes carry the same weight. The four below are the ones that will hurt you first.

The invoice number race condition is a bug that does not manifest until you have concurrent users, which means your very first real traffic spike is also the first time you see it. Fix: move number generation into a Postgres sequence, serve it via an RPC, return the authoritative number from the database after insertion. One migration, thirty lines, gone forever. The general principle is broader: any identifier that needs to be unique cannot be generated on the client.

The unscoped queries were a defense-in-depth issue. RLS held in practice, but "the app does not currently leak data because one specific layer is configured right" is not a production posture. Every query now filters explicitly, RLS was tightened, and a test verifies that a user cannot retrieve another user’s data even with RLS deliberately weakened.

TypeScript strict mode surfaced two dozen real latent bugs once turned on. Strict mode is the cheapest possible way to surface the places where "almost" is hiding, which is the top complaint about AI-generated code in 2026.

The dashboard chart fix was almost cosmetic at prototype scale. The reason to fix it is not the milliseconds, it is that one O(12 × n) loop in a codebase usually means twenty more places where the same pattern exists. Fixing it is partly about the chart, mostly about retraining the codebase’s habits.

Same UI, production-grade underneath screenshot
// next step

Got a Lovable, Bolt, or Cursor app starting to wobble?

Book a production audit. A senior engineer reads your code, runs the diagnostics, and gives you the honest answer. If there is nothing wrong, we will tell you that. If there is a lot wrong, we will tell you that too, in order of how fast it will bite you.

Book a production audit