A product built with Lovable, Bolt or Cursor usually reaches the point where every fix breaks something else. The instinct is to rewrite it. The cheaper first step is to write down what it does, because the running app is the only specification you have.
The code works until someone changes it
Generated code is written one prompt at a time. Each prompt fixes the problem in front of it, and nothing holds the whole design in its head. After a few months the same rule lives in four places, a page reads from a table nobody remembers adding, and the error handling is whatever the model guessed that day.
None of that shows while you click through the app. It shows when you change it. A small edit to the checkout touches a copy of the price logic that the cart doesn't use, and the two disagree. Nobody wrote a test, so nobody finds out until a customer does.
A rewrite throws away what the app knows
Your users depend on behaviour you never wrote down. The odd rounding on invoices, the way a cancelled order still sends a receipt, the field that is optional on one form and required on another. Some of it is a bug and some of it is the product.
A rewrite from a fresh prompt or a fresh team keeps the parts somebody remembers and loses the rest. Then the new version has its own bugs on top. Rewrites are sometimes right, and we cover when below. They are rarely the right first move.
Write down what it does, then test that
Start with the flows that make you money or would hurt you if they broke. For each one, write the rules it must always follow, in plain sentences. An order total is never negative. A user only ever sees their own records. A refund never exceeds the payment.
Then turn each rule into a property-based test. Instead of three hand-picked examples, the test generates thousands of inputs and checks the rule on every one. This is where generated code gives itself away, at the empty cart, the currency with no decimals, the name with an apostrophe.
Where a test fails, decide whether the code or the rule is wrong. That decision is the real work, and it needs someone who knows the business. At the end you have a specification you can read and a suite that checks it on every change.
Cut against the tests
Now the cleanup is safe. Remove the dead paths, merge the four copies of the price rule into one, and delete the layers a model added "just in case". After each cut the tests run. If they pass, behaviour you care about hasn't changed.
This is also when types earn their keep. Put the constraints the tests found into the types, so the next prompt can't quietly break them. A cleaned-up codebase is usually smaller than the one you started with, and that is the point.
Check the security basics first
Before any of this, look for the problems that cost more than a bug.
- Keys and secrets that ended up in code the browser downloads.
- API routes that check the user is logged in but never check what that user is allowed to see.
- Database access rules. If the app uses Supabase, confirm row-level security is on for every table, with a policy that matches who should read each row.
- Admin pages that are hidden but not protected.
These are quick to check and slow to explain to a customer.
When a rewrite is right
Rewrite when the app's job has changed so much that the old behaviour no longer matters, or when the stack can't do what you need next at any price. Even then, write the specification first. It tells the new build what it has to match, and it tells you when the rewrite is done.
If your app has reached the stage where nobody wants to touch it, tell us what you built. We'll say which of these steps it needs first.