Anatomy of a Vibe-Coded SaaS: Eleven Things You Can Grep For Before Lunch

October 23, 20268 mins
Anatomy of a vibe-coded SaaS — Unsplash
You can learn most of what matters about a vibe-coded codebase without reading it. You need a terminal and about thirty minutes.

Each check below is a fingerprint. The first six show how the project was run: whether anyone reviewed anything, and where the agent put what it didn't understand. They're smells, not bugs. The last five find damage: code that works on a laptop and breaks in production, or leaks something it shouldn't.

Run them from the repository root. Swap src for wherever your code lives, and add --exclude-dir=node_modules if library code floods the output.

1. fix, then actual fix

git log -100 --format=%s | grep -ciE '^(fix|update|wip|asdf)

This counts commit messages that say nothing. Our rule of thumb: more than 20 out of the last 100 means nobody was reviewing. Commits were save points, not decisions.

2. A README that's mostly emojis

Not a grep. Open it. A star next to every feature, and a "Why I built this" section longer than the install steps. The codebase is a personal blog with extra steps.

3. useState where a reducer belongs

grep -rc "useState(" src --include="*.tsx" | sort -t: -k2 -n | tail

This lists the files with the most state hooks. Vibe-coded React tends to grow parallel pieces of state that should be one reducer. A component with a dozen of them is where the next bug lives.

4. A helpers file that ate the project

find src \( -name "*util*" -o -name "*helper*" \) -type f | xargs wc -l | sort -n | tail

Anything past 2,000 lines is where the agent dumped what it didn't know where to put. Look for duplicates while you're in there: three versions of formatDate(), two slugify() helpers.

5. // TODO: fix later next to a real bug

grep -rnE "TODO|FIXME" src | wc -l

Each one is a decision someone put off. Agents leave them when they aren't sure. Founders rarely come back.

6. Two ORMs

grep -oE '"(@prisma/client|drizzle-orm|typeorm|sequelize|mongoose|kysely)"' package.json

More than one line of output means two ways to talk to the same database. Somebody asked "what's the best ORM" in two sessions and kept both answers, each with its own migrations.

This counts commit messages that say nothing. Our rule of thumb: more than 20 out of the last 100 means nobody was reviewing. Commits were save points, not decisions.

2. A README that's mostly emojis

Not a grep. Open it. A star next to every feature, and a "Why I built this" section longer than the install steps. The codebase is a personal blog with extra steps.

3. useState where a reducer belongs

__CODE_BLOCK_1__

This lists the files with the most state hooks. Vibe-coded React tends to grow parallel pieces of state that should be one reducer. A component with a dozen of them is where the next bug lives.

4. A helpers file that ate the project

__CODE_BLOCK_2__

Anything past 2,000 lines is where the agent dumped what it didn't know where to put. Look for duplicates while you're in there: three versions of formatDate(), two slugify() helpers.

5. // TODO: fix later next to a real bug

__CODE_BLOCK_3__

Each one is a decision someone put off. Agents leave them when they aren't sure. Founders rarely come back.

6. Two ORMs

__CODE_BLOCK_4__

More than one line of output means two ways to talk to the same database. Somebody asked "what's the best ORM" in two sessions and kept both answers, each with its own migrations.

Turn the eleven checks into commands for your repo

Tell ChatGPT your stack and folder layout. It will rewrite each check as a command for your repo and say what counts as a red flag.

“|”

7. Hardcoded localhost

grep -rn "localhost:[0-9]" src

A fetch("http://localhost:3000/api/...") works on your laptop, where something is listening on port 3000. Deployed to Vercel, nothing is, and the call fails. Often silently, which brings us to check 9.

8. NEXT_PUBLIC_STRIPE_SECRET_KEY

grep -rnE "(NEXT_PUBLIC|VITE)_[A-Z_]*(SECRET|SERVICE_ROLE|PRIVATE)" . --exclude-dir=node_modules

Next.js ships every NEXT_PUBLIC_ variable to the browser, and Vite does the same with VITE_. Anyone who opens dev tools can read a secret under either prefix. A leaked Stripe secret key lets a stranger read your customer list and issue refunds as you. If this returns anything, rotate the key today.

9. Empty catch

grep -rnE 'catch *(\([^)]*\))? *\{ *\}|\.catch\(\(\) *=> *\{ *\}\)' src

catch (e) {} swallows the error and moves on. Agents write it when the prompt was "make it work". It works until the day it quietly doesn't, and the logs say nothing.

10. A migration named add_ that drops a column

grep -rli "drop column" supabase/migrations prisma/migrations drizzle migrations 2>/dev/null | grep -i add

Rename a field in a Prisma or Drizzle schema and the generated migration can come out as a drop plus an add. Development data is fake, so nobody notices. In production, the old column's data is gone. Read every file this returns.

11. Zero tests, one console.log("here")

find . -name node_modules -prune -o \( -name "*.test.*" -o -name "*.spec.*" \) -print | wc -l
grep -rn "console.log" src | wc -l

A zero on the first line and a big number on the second is a debug session that shipped. When we took over Oria, a platform built fast with AI coding agents, there were no tests at all, and secrets were hardcoded in the code.

What to fix first

Almost everything here is fixable. Most of it is also fixable in the wrong order.

Lock down secrets first, then authorization, then the data layer. Tests come after that, and new AI features last. Founders tend to run it backwards and ship a new feature while the leaked key is still live.

Ran the eleven checks and didn't like what came back?

We audit and fix AI-built codebases at a flat price. Rescue audit and plan from $500: a written audit in 48 hours and a flat-price plan for the fix. Send your email and we'll set up a 30-minute call within 24 hours.

How to read your results

  • Hits on checks 1 to 6 only. The project was built fast and nobody tidied up. Fixable, not urgent.
  • Any hit on check 8. Stop and rotate the key. Then carry on.
  • Two or more hits among checks 7, 9, 10 and 11. Production is less stable than it looks, and swallowed errors hide it. That's where a second pair of eyes pays for itself.

The check grep can't do

Eleven greps can't tell you whether one customer can read another customer's data. For that, log in as users in two organizations and try. It was the finding that mattered most at Oria, and no grep would have caught it.

Related reading

Built with a specific tool? Cursor, Lovable, Bolt, Claude Code. The wider picture: vibe-coded app rescue. The service: Vibe Code Rescue.

Enjoyed this article? Share it with others

Related Posts