What a production-ready app actually needs
The gap between 'it works on my machine' and 'it's running in production reliably' is where most vibe-coded projects fall apart. Here's what actually needs to be in place.
There's a version of "working" that means the app does the right thing when you click the right buttons in the right order on your laptop. And there's a version of "working" that means it runs reliably for real users, handles unexpected input, survives traffic spikes, logs errors when something goes wrong, recovers from failures, and doesn't leak user data.
Most vibe-coded apps are the first kind. Getting to the second kind is the actual work.
This isn't a criticism of AI-assisted development — I use it heavily myself. It's a description of where the tools currently stop. AI is very good at building features. It is much less good at building the infrastructure that lets a feature run safely at scale, because that infrastructure is largely invisible until something breaks.
Here's what actually needs to be in place before an app is ready to run in production.
Authentication and authorisation that isn't an afterthought
Authentication — checking who you are — is the part most vibe-coded apps get right. There's usually a login screen, usually a token of some kind, usually a session.
Authorisation — checking what you're allowed to do — is where things go wrong.
The typical failure is that access control lives at the route level: the UI only shows you certain buttons, and certain API endpoints check whether you're logged in before responding. But the data layer has no idea who's asking. If someone sends a request with a valid token but asks for someone else's data — by changing an ID in a URL, by enumerating records, by crafting a slightly different API call — the database just answers.
This is called an IDOR vulnerability (Insecure Direct Object Reference), and it's one of the most common security issues in quickly-built applications. The fix is to scope every query to the authenticated user. Not at the route. At the query. Before anything else in a production readiness review, I want to see that every database call is written as "give me the records that belong to this user", not "give me record 47."
Similarly: admin and privileged functions need to be authorised at the server, not just hidden in the UI. A frontend that doesn't show the delete button is not the same as a backend that won't perform a deletion for an unauthorised user.
Environment configuration that isn't in the repository
This one is still surprisingly common.
API keys, database credentials, third-party service tokens, JWT secrets — these should live in environment variables, loaded at runtime, never committed to the repository. If they're in the codebase, anyone who has ever had access to that repository has those secrets. If it's ever been public, even briefly, assume they're already compromised.
A production-ready app has:
- A
.env.examplefile showing what variables are required, with no real values - A
.gitignorethat explicitly excludes.envand.env.local - Secrets stored in a proper secret management system (AWS Secrets Manager, Doppler, or at minimum a private environment configuration in your deployment platform)
- Secrets rotated as part of any security review
The rotation is important. If credentials have been in a repository, it's not enough to remove them — they need to be cycled. The old ones need to stop working.
Error handling and observability
Most applications built quickly have good happy paths. The error paths are where they fall apart.
What happens when a third-party API your app depends on goes down? What happens when a database query times out? What happens when a user submits a form with data that doesn't match what the app expects? In many vibe-coded apps, the answer is: a generic error, a silent failure, or a crash that nobody finds out about until a user complains.
Production-ready error handling means:
Structured error responses. API errors should return consistent formats that the frontend can handle gracefully, not raw stack traces or unformatted messages.
Logging that captures context. When something goes wrong, you need to know what happened, when, for which user, with what input. A log that says "Error: undefined" is not useful. A log that says "Payment processing failed for user ID 12345, stripe error: card_declined, amount: $49, timestamp: ..." is.
Alerting on real failures. Errors should be captured by a monitoring tool (Sentry is the standard; Datadog if you're running more infrastructure) that aggregates them, surfaces the most frequent, and can alert on spikes. You should not be finding out about production errors from customer support emails.
Graceful degradation. When a non-critical service fails, the rest of the app should keep working. If your email service goes down, users should still be able to complete their order — they might just not get a confirmation email immediately.
A deployment process that isn't "push to main"
The most dangerous thing I see in young applications is a single environment that is simultaneously development, staging, and production.
Changes go straight to the live system. Testing happens on real user data. A broken deployment takes down the whole product. Rollbacks mean going back through git history manually.
A production-ready deployment setup looks like this:
Separate environments. At minimum: a development/staging environment and a production environment. They should be isolated from each other. Staging should have its own database, its own configuration, and should be as close to production as possible without being production.
Automated deployments. Changes to the codebase should deploy through a CI/CD pipeline that runs tests before deploying to staging, requires a deliberate step to promote to production, and can be rolled back quickly if something goes wrong.
Zero-downtime deployments. For anything with real users, a deployment that takes the app offline — even for 30 seconds — is not acceptable. Rolling deployments, blue-green deployments, or similar strategies ensure that users don't notice when you release.
Database migration safety. Every schema change should be handled through migrations that are backwards-compatible during the deployment window. The most common cause of downtime during a release is a database change that breaks the current code before the new code is deployed.
Data validation at the server
User input cannot be trusted. This isn't cynicism — it's the first principle of secure application development.
Frontend validation is a UX feature. It tells users immediately when something is wrong, before they hit submit. But it cannot be relied on for correctness or security, because it can be bypassed by anyone with basic developer tools.
Server-side validation needs to check everything independently: data types, lengths, formats, ranges, referential integrity. If an endpoint expects a number and receives a string, it should return a validation error. If an endpoint expects a value from a known set and receives something else, it should reject it.
SQL injection, command injection, and XSS vulnerabilities all have roots in the same place: input that was trusted and used without being checked. The fix is the same in all cases: validate and sanitise everything that comes from outside your system, every time, at the server.
Backups that have actually been tested
Most production databases are backed up. Most developers have never tried to restore one.
A backup that hasn't been tested is a backup you don't know works. It may be incomplete. It may be in a format that's harder to restore from than expected. The restoration process may take longer than you planned.
Production-ready data management means:
- Automated backups on a regular schedule (daily at minimum; more frequent for high-transaction systems)
- Backups stored somewhere separate from the primary database (if your database provider goes down, your backups shouldn't be on the same infrastructure)
- A tested restoration procedure — meaning someone has actually run through restoring from a backup at least once
- A documented recovery time objective — how long can the product be unavailable before it becomes a serious problem, and does your current backup/restore process meet that?
A practical approach to getting there
If you're running an app that doesn't have all of this in place, you don't need to stop everything and rebuild. Most of it can be added incrementally, starting with the highest-risk gaps.
The order I usually work in:
- Secrets — get credentials out of the repository and into proper environment configuration. This takes a few hours and eliminates a whole category of risk.
- Authorisation — audit every data-fetching endpoint to confirm it scopes to the authenticated user. This is the most critical security item.
- Error monitoring — add Sentry or equivalent. Takes an hour; immediately tells you what's actually breaking in production.
- Environments — separate staging from production. This makes every future change safer.
- Backups — verify they're running, then actually test a restore.
- Validation — add server-side validation to the most sensitive endpoints first, then work outward.
None of this is novel or difficult in isolation. The challenge is that AI-assisted development optimises for speed of feature delivery, not for the presence of infrastructure that's invisible when it's working. Getting it in place is a deliberate step — and usually a worthwhile one, because the alternative is finding out about each gap when it becomes a production incident.
If you're at that point — you've got something running, it's doing useful work, but you're not confident it's solid — we're happy to take a look. That kind of review and hardening is something we do regularly.
This is part of a series on AI-assisted development and what it means for software quality. See also: Your developer is vibe coding too — here's why it's different and How to rescue a vibe-coded app.