All posts
EngineeringMay 17, 20267 min read

How to Make Your Vibe Coded App Production Ready

You built it fast with AI. It works on your machine, looks good in the demo, and you're ready to ship. Before you point real users at it, spend a few hours on the things that separate a weekend project from something that actually holds up. This is that checklist.

What "Production Ready" Actually Means

Production ready does not mean perfect. It means your app will not embarrass you when a real user hits an edge case, a bad actor probes for weaknesses, or traffic spikes at an inconvenient time. It means you have thought past the happy path — that the error states, the missing data, the unexpected inputs, and the concurrent requests have all been considered. Most vibe-coded apps have great happy paths and catastrophic everything-else. That is the gap this post closes.

The 5 Biggest Issues Vibe Coded Apps Have

1. Security holes hidden in plain sight

AI models are optimistic. They generate code that works for the expected input and ignore what a malicious or careless user might actually send. The most common result is SQL injection, missing auth checks, and wildcard CORS settings.

Here is a real pattern that shows up constantly in vibe-coded backends:

❌ vibe coded — SQL injection waiting to happen
// user controls req.query.id — they can inject anything
const result = await db.query(
  `SELECT * FROM users WHERE id = ${req.query.id}`
);
✅ production ready — parameterized query
// input is passed as a parameter, never interpolated
const result = await db.query(
  'SELECT * FROM users WHERE id = $1',
  [req.query.id]
);

One character difference in intent, catastrophic difference in outcome. The AI-generated version works — until someone sends 1 OR 1=1 as the ID and gets back every row in your table.

2. No error handling — the silent killer

Vibe-coded apps fetch data. When the fetch works, everything is fine. When it fails — a timeout, a 500 from the upstream API, a malformed response — the app either crashes silently or shows a blank screen with no explanation. Users churn. You find out three days later when someone emails you.

❌ vibe coded — unhandled rejection
async function loadDashboard(userId) {
  const data = await fetch(`/api/dashboard/${userId}`);
  return data.json(); // throws if response is not ok
}
✅ production ready — every failure is handled
async function loadDashboard(userId) {
  try {
    const res = await fetch(`/api/dashboard/${userId}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    // log to your error tracker, return a safe fallback
    reportError(err);
    return null;
  }
}

The rule is simple: every await that touches the network or a database belongs inside a try/catch. No exceptions.

3. No tests on the code that matters most

You do not need 100% test coverage to ship. You need tests on the code where a bug is catastrophic: your authentication logic, your payment flow, your data mutation endpoints. These are the paths where a silent regression costs you users, money, or trust.

If your project has zero tests, start small. Write one test for your login function. Assert that it rejects invalid credentials and accepts valid ones. That single test has caught real bugs in production systems more times than it has any right to.

If you are not sure where to start, paste your auth file into your AI tool and ask it to write a Jest test suite covering every exported function. You will have something in five minutes.

4. Monolithic files that no one can navigate

AI generates code fast, and it tends to put everything in one place. Left unchecked, you end up with a utils.ts that is 800 lines long, an api.ts that handles 15 different concerns, and a React component that manages global state, fetches data, handles form validation, and renders the UI all in one 600-line function.

This is called a god file, and it is one of the strongest signals that a codebase is not production ready. It makes bugs harder to find, changes harder to make safely, and onboarding anyone else nearly impossible.

The fix is not elegant refactoring — it is a simple rule: if a file exceeds 300 lines, split it. Pull out related functions into their own modules. Separate your data-fetching logic from your rendering logic. The goal is files where you can understand the entire surface area in 30 seconds.

5. Hardcoded secrets in the repository

This one is embarrassingly common and genuinely dangerous. AI code generation often produces working examples with credentials inline because it is optimizing for making the code run, not for security. Those credentials get committed, the repository goes public, and bots scraping GitHub for exposed API keys find them within minutes.

❌ vibe coded — live key committed to git history
const stripe = new Stripe('sk_live_4eC39HqLyjWDarjtT7en');
const openai = new OpenAI({ apiKey: 'sk-proj-abc123...' });
✅ production ready — keys in environment variables
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

Even if your repo is private today, treat every secret as if it will be public tomorrow. Add a .env.example listing every required key (with placeholder values), confirm your .gitignore covers .env, and rotate any key that has ever touched a commit.

Your Pre-Ship Checklist

Work through this before you send your first real user to the app. None of these items require rewriting your codebase — they are targeted fixes that buy you a disproportionate amount of safety.

Pre-Ship Checklist

Security

Move all secrets to environment variables
Add authentication to every private route
Validate and sanitize all user inputs
Set CORS to specific origins only
Run npm audit and fix critical CVEs

Resilience

Wrap every async call in try/catch
Return meaningful error messages to clients
Add loading and error states to every UI fetch
Handle null / undefined at API boundaries
Set timeouts on external HTTP calls

Code Health

Split files over 300 lines into modules
Delete commented-out code and console.logs
Write at least one test for your auth flow
Add a .env.example with all required keys
Check your .gitignore covers .env files

If you check everything in that list, you have done more production hardening than the majority of vibe-coded apps that ship. That is not a high bar, but it is a meaningful one.

How to Find What You Missed

The honest problem with self-review is that you cannot see your own blind spots. You wrote the code with a mental model of how it works, and that same mental model filters out the failure modes you did not think of. This is why code review exists in professional teams — a second pair of eyes catches what the first pair normalizes.

If you are working alone, the closest thing to that second pair of eyes is a tool that looks at your codebase without your assumptions. GitDoctor runs 70+ checks across security, error handling, code quality, test coverage, and dependency health — then surfaces every issue with a severity rating and a ready-to-use AI prompt to fix it. You get the kind of feedback a senior engineer would give in a code review, without needing to find one.

Point it at your repository before you launch. It takes under a minute, and the findings will tell you exactly where to spend your last few hours of pre-launch time. Fixing one critical security issue before launch is worth more than any feature you could add in the same time.

Run the checks

Find out what your app is missing before users do

GitDoctor scans your GitHub repo and surfaces security holes, missing error handling, and code quality issues — with AI prompts to fix each one. First scan is free.

Scan your repo