If you're running a business, "it compiles" is not a security policy. Vibe coding is great for proving a product has market interest. Shipping prototype-grade logic to paying users is how you lose database integrity, customer trust, and MRR. These are the seven failure modes we find in nearly every AI-built SaaS we rescue — what each looks like, why the model produced it, the smallest fix, and the public receipt.
1. Endpoints trust the client
A profile-update endpoint takes the JSON body and writes it straight to the DB. An attacker adds `"role": "admin"` to the payload, and because nothing validates the shape, the write goes through. The model assumes the only thing hitting your API is your own frontend — it's never met curl or Postman. Fix it with a strict schema at the boundary:
import { z } from 'zod';
const profileUpdateSchema = z
.object({
name: z.string().min(1).max(100).trim(),
email: z.string().email().max(255),
})
.strict(); // rejects extra keys like 'role'
app.put('/api/user/profile', async (req, res) => {
const v = profileUpdateSchema.safeParse(req.body);
if (!v.success) return res.status(400).json({ error: v.error.issues });
await db.user.update({ where: { id: req.user.id }, data: v.data });
res.status(200).json({ success: true });
});Receipt: Veracode tested 100+ models — 45% of generated code introduces an OWASP Top 10 issue. Founders auditing their own vibe-coded repos report critical vulns in 68% of them, with input validation the most common.
2. Auth exists, authz doesn't
The user logs in, lands on `/projects/123`, changes the URL to `/projects/124`, and reads another customer's data. Classic IDOR. To the model, auth is a solved library drop-in; authorization — which user owns which row — is a system-level relationship it doesn't map. Scope every query to the session user, and 404 (don't 403) so you don't leak existence:
app.get('/api/projects/:id', async (req, res) => {
const project = await db.project.findFirst({
where: { id: req.params.id, ownerId: req.user.id },
});
if (!project) return res.status(404).json({ error: 'Project not found' });
res.json(project);
});Receipt: CodeRabbit found AI code carries 2.74x more security vulnerabilities than human code; Veracode clocked privilege-escalation paths up 322% in AI-assisted codebases.
3. Secrets in the repo
Your live Stripe key sits in a config file, committed to history because the AI never generated a `.gitignore`. Models copy quickstart docs that hardcode keys to minimize setup friction. Strip them, scan history with gitleaks/trufflehog, rotate anything exposed, and read from the environment with a runtime assertion:
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error('CRITICAL CONFIG MISSING: STRIPE_SECRET_KEY');
}
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);Receipt: AI-assisted commits leak secrets at 3.2% vs 1.5% for humans (CSA). GitGuardian found 28.65M hardcoded credentials in public commits in 2025 — a 34% jump year over year.
4. Silent failures
The user clicks the main action, the DB write fails, and the UI shows a green checkmark anyway — the error got caught, logged to a stream nobody reads, and a 200 went back. The model wraps async flows in loose try/catch so demos never crash. Map exceptions to real HTTP states and log structured JSON:
import pino from 'pino';
const logger = pino();
app.post('/api/billing/charge', async (req, res) => {
try {
const tx = await processPayment(req.body);
res.status(200).json({ success: true, transactionId: tx.id });
} catch (err) {
logger.error({ err, payload: req.body }, 'Payment processing failure');
res.status(502).json({ error: 'Payment failed. Please try again.' });
}
});Receipt: A Hacker News survey found 43% of AI-generated changes need manual debugging in prod — subtle async/logic bugs that pass unit tests and fail live.
5. No indexes / N+1 queries
Fast at 100 rows, eight seconds at 10,000, DB CPU pinned. The model has never run a query under load — it loops and fires one query per parent row, and never writes the migration to index your foreign keys. Use a join / eager load and add the indexes:
const orgsWithMembers = await db.organization.findMany({
include: { members: true }, // single optimized join instead of a loop
});Receipt: r/SaaS engineering leads keep flagging the same "hollow engineering" — AI code that ignores indexing, pooling, and locking, then falls over at minimal traffic.
6. Dead-code rot
Seventeen versions of a date helper, orphaned interfaces, unreferenced exports. The model writes side-by-side helpers instead of touching code it doesn't fully grasp, which feeds the context decay loop — every duplicate dilutes the AI's context and forces more errors next prompt. Gate it in CI:
npx ts-prune # find unreferenced exports
# tsconfig.json: "noUnusedLocals": true, "noUnusedParameters": trueReceipt: GitClear: refactoring dropped from 25% of changed lines (2021) to under 10% (2024) while duplicated code quadrupled.
7. Naive billing retries
A card declines on renewal and the webhook either hard-deactivates the account or retries four times instantly and gives up. The model wrote billing as a two-state flow — it doesn't know decline codes, regional banking, or payday cycles. Branch on the decline reason:
app.post('/webhook/stripe', async (req, res) => {
const invoice = req.body.data.object;
if (req.body.type === 'invoice.payment_failed') {
const code = invoice.last_payment_error?.decline_code;
if (code === 'insufficient_funds') {
await scheduleSmartRetry(invoice.subscription, { waitDays: 3 }); // time to payday
} else {
await triggerCustomerDunningEmail(invoice.customer); // hard decline -> email
}
}
res.sendStatus(200);
});Receipt: `insufficient_funds` is ~44% of failed charges; retry within 24h and you recover 45–55%, wait past 8 days and it's under 15%. Naive retries cost 8–12% of MRR in preventable involuntary churn. (This is where the boring layer starts — tenancy, durable jobs, billing recovery.)
Stop generating, start hardening
You don't tear the app down. You verify what the model built: a validation layer, mapped authorization, hardened queries, real error handling, indexes, clean exports, and a dunning engine that respects decline codes. Do that before you scale marketing spend, not after the incident.
That's exactly the work we do — our custom platform team finds, patches, and hardens all seven in place.
