AIVibe CodingProduction

Vibe Coding vs. Production Reality

AI coding tools let anyone ship a working prototype in a weekend. What they don't tell you is what happens when real users show up.

Forte TeamApril 3, 20267 min read

Vibe Coding vs. Production Reality

You built a SaaS in a weekend with Claude. Not a toy — something real. A Stripe integration, a user signup flow, a dashboard that queries an external API and renders the data in a table. It took you maybe twelve hours spread across Saturday and Sunday. You prompted your way through the parts you didn't know, copy-pasted the generated code, tweaked a few things, and it worked.

A friend tries it. It works. You post it on Indie Hackers with a "Show HN"-style writeup, get 200 upvotes, and 50 people sign up in the first hour.

Then the pain starts.

Response times climb from 200ms to 8 seconds. Your free-tier database starts rejecting connections. Someone DMs you saying they logged in and saw another user's data — actually saw it, with their name and email address in the header. And then a message from someone you've never heard of: "Hey, quick heads up — I found your API key in a public GitHub commit."

This is not a hypothetical. Some version of this story plays out hundreds of times a month.

What "Vibe Coding" Actually Produces

To be clear: AI coding tools are legitimately impressive. A Claude-generated Express app handles the happy path brilliantly. The route handlers are clean. The schema validation looks reasonable. The Stripe webhook integration works on the first try. For getting from zero to a working demo, there is genuinely nothing faster.

But there are things a vibe-coded app typically doesn't have — not because the AI is bad, but because these things simply don't matter until they matter. And they all tend to matter at once.

No rate limiting. Your API accepts requests at whatever speed clients send them. That's fine with one user testing on localhost. It is not fine when someone's automation script hammers your /api/generate endpoint 3,000 times in a minute.

Sessions stored in memory. The most common scaffolded auth setup stores session state in your Node process. Restart the server and everyone gets logged out. Run two instances of your app and session lookups fail half the time — or worse, return the wrong user's session, which is exactly how "I saw someone else's data" happens.

No error handling at the infrastructure layer. When something throws an unhandled exception, your app crashes and nobody knows until a user reports it. There's no retry logic. There's no alerting. There's no circuit breaker. The process just dies.

Secrets in your git history. The .env file that holds your Stripe secret key, your database connection string, your API keys — it's been committed to your repo at least once. You deleted it in the next commit, but git history is forever. If the repo is public, those credentials are gone.

Auth is bolted on, not designed in. The session middleware was added to the top of app.js after the routes were already written. Some routes check for authentication. Others don't. Which ones? You're not entirely sure.

Logging is console.log. When something goes wrong in production, you have no structured logs, no request IDs, no trace of what happened before the error. You have whatever happened to be printed to stdout. If the process restarted, you have nothing.

None of this is a critique of the developer who wrote it. These are the things that don't show up when you're testing alone on your MacBook. They show up when real users arrive.

The Deployment Cliff

At some point you decide to do this properly. Not on a free Render tier or a Vercel hobby plan — you want something production-grade, something that can handle growth.

This is where you discover the deployment cliff.

You go to containerize your app and realize you have no idea what a Dockerfile looks like for a Node.js app with a native dependency. You find a template, copy it in, and the build fails because your app assumes a specific Node version. You fix that and the container builds, but now it won't start because process.env.DATABASE_URL is undefined — the .env file you were relying on isn't included in the container image.

You fix the environment variables by setting them manually in the PaaS UI. But now you need to configure a health check. What endpoint should it hit? What status code should it return? Your app doesn't have a /health endpoint. You add one. The health check still fails because it's checking port 80 and your app listens on 3000.

Meanwhile, you're being billed for a container that hasn't started yet.

Then you scale to handle the traffic. Your free tier handled one concurrent user. Twenty concurrent users hit a database connection limit you didn't know existed. You upgrade the database tier and the monthly cost is now more than you've earned from the product.

The deployment cliff isn't one problem. It's ten problems that show up simultaneously, each blocking the next, none of them obvious until you're already in the middle of them.

The Gap Between "It Works" and "It Works Under Pressure"

The specific things that vibe-coded apps tend to ignore are not exotic. They're table stakes. Here's what actually breaks:

Session management. Memory-based sessions don't survive a process restart. They don't survive a deployment. They definitely don't survive running two instances of your app behind a load balancer, which is the minimum requirement for any kind of availability guarantee. The fix is storing sessions in a database — but that requires configuring a session store, and now you need to think about session expiry, session invalidation, and what happens when the session store is unavailable. None of this is in the scaffolded code.

Auth at the application layer. Writing auth middleware once at the top of your router doesn't mean it applies to every endpoint. Middleware order matters. Route configuration matters. A misconfigured route — one router.get('/admin/users', handler) without the auth middleware applied — is an unauthenticated endpoint in production. Miss one and someone will find it.

Error observability. console.log gets you through localhost. It does not get you through a production incident. When an error happens in production and you can't reproduce it, you need structured logs with timestamps, request IDs, user context, and the full stack trace. Without that, debugging is guesswork. Real observability is a first-class concern, not an afterthought you can add later.

Cost at scale. The arithmetic that works for a hundred users breaks at ten thousand. A database query that runs in 40ms with 500 rows takes 4 seconds with 500,000 rows. An API call that costs $0.001 per request costs $1,000/month at moderate scale. These numbers are not alarming in isolation — they're alarming when you're paying them before you've validated that anyone will pay you.

Cold starts. That 8-second first load when a user hits your app after it's been idle? That's your container spinning up from zero. Free and low-cost tiers scale to zero when there's no traffic, which means the first user after any quiet period waits for your entire stack to initialize. Real users don't wait. They leave and don't come back.

The cold start problem

A container that takes 8 seconds to start is a container that loses first impressions. If your signup flow has a cold start, a meaningful percentage of your Indie Hackers traffic never sees it.

The Options When You Hit the Cliff

When this all lands on you at once, you have a few options. They're worth being honest about.

Hack it. Keep adding fixes to the existing stack. Add a Redis session store. Add rate limiting middleware. Add a Sentry SDK. Add a health check endpoint. Add a secrets manager integration. Each of these is a reasonable fix in isolation. Together, they become a maintenance burden that compounds over time, because none of it was designed to work together. You end up with five different configuration surfaces, four different vendor dashboards, and a deployment process that breaks whenever you touch it.

Learn AWS from scratch. This is the correct long-term answer for anyone running infrastructure seriously. It is also 3 to 6 months of real learning before you're competent enough to not make expensive mistakes. If you're trying to iterate on a product and validate a business, you don't have 6 months to become an AWS expert. You'll get there eventually, but it's not the answer to the fire you're currently in.

Pay for the tools separately. Auth0 for authentication. Datadog for observability. Heroku (or Railway, or Render) for hosting. Sentry for error tracking. This is a legitimate stack. It's also $300–$500/month before you have a single paying user, each with its own billing cycle, its own API to learn, its own SDK to integrate, and its own failure mode.

Find a platform that handles the boring parts. This is the answer most people arrive at eventually, usually after spending two weeks on the previous options. A platform that handles session persistence, health checks, deployment pipelines, environment variable management, and logging — not as separate paid add-ons, but as infrastructure defaults — removes the deployment cliff almost entirely.

That's not a Forte pitch. It's just the honest answer to why platforms that abstract over infrastructure exist. The complexity doesn't go away; it gets pushed somewhere you don't have to manage manually.

What Production-Ready Actually Means

"Production-ready" is a phrase that gets thrown around in ways that make it sound either trivially obvious or impossibly complex. It's actually fairly concrete.

Auth that doesn't break when you scale. Sessions stored in a persistent store, not in process memory. Token validation that happens at the infrastructure layer, before requests reach your application code. Session invalidation that actually works.

Containers that restart on failure. When your process crashes, something needs to notice and restart it. When your deployment fails, something needs to roll back. When traffic spikes, something needs to spin up additional capacity. None of this happens automatically with a process running on a server you SSH into.

Logging that tells you what happened, not just that something happened. Structured logs with request IDs, timestamps, user context, and stack traces. The ability to find the logs for a specific request, not just search for keywords and hope.

Environment variables that aren't in your git history. Secrets that live in a secrets manager or environment configuration system, injected at runtime, never written to disk, never committed to source control.

A deployment pipeline that doesn't require you to SSH into a server. Push to your main branch. Something builds a container, runs your tests, deploys to production, and notifies you if it fails. No manual steps. No crossing your fingers.

None of this is glamorous work. None of it ships features. All of it matters the moment you have customers who are counting on the thing to work.

The Gap Is Infrastructure

The gap between "I built a thing" and "I run a product" is almost entirely infrastructure. The code is rarely the hard part anymore — AI tools have genuinely changed that. The hard part is the layer underneath the code: deployment, sessions, secrets, observability, scaling, cost management.

AI tools have made building faster. They haven't made running easier.

If you're at the point where your vibe-coded app has real users and real traffic and you're starting to feel the cliff, the path forward isn't rewriting the app. It's getting serious about the infrastructure layer. That might mean learning it yourself, paying for it piecemeal, or finding a platform that bundles it. Forte was built specifically for this moment — the transition from working prototype to production product — but whatever path you take, the sooner you close the gap, the better.

The 8-second cold starts and the session leaks and the missing rate limiting are all solvable problems. They just don't solve themselves.