AI-Built Apps

I Vibe-Coded an App — Now What? The Production Readiness Checklist

You described an app to an AI, iterated for a weekend, and now there is something that actually works on your laptop. The gap between that and something real users can hit is wider than it looks — and most of it has nothing to do with the code the model wrote. To deploy a vibe-coded app to production, you have to make a handful of decisions the AI quietly skipped: where secrets live, where data lives, how traffic reaches you over HTTPS, what happens when something throws, and how you find out when it does.

Here is the checklist I run through before I call any generated app "shipped." Work top to bottom; each item is a place I have seen weekend projects fall over in their first week.

Your production readiness checklist to deploy a vibe-coded app

Five categories, in priority order: secrets, database, domain and TLS, error handling, and monitoring. Do the first three before you share a link with anyone. Do the last two before you share it with anyone you care about impressing.

1. Get secrets out of the code

AI tools love to inline API keys. Search your repo before anything else:

# Find likely secrets committed to the repo
git grep -nE "(sk-[a-zA-Z0-9]{20,}|AKIA[0-9A-Z]{16}|-----BEGIN)"

# And check what's already tracked
git ls-files | grep -E "\.env($|\.)"

If a real key shows up, treat it as leaked: rotate it at the provider, then remove it from the code. Keys belong in environment variables, and the values belong in your host's secret store — never in the repo. A minimal .env for local work:

# .env.local  (git-ignored, never committed)
OPENAI_API_KEY=sk-...
DATABASE_URL=postgres://user:pass@localhost:5432/app
JWT_SECRET=$(openssl rand -hex 32)

Make sure .gitignore actually covers it (.env, .env.*, !.env.example) and commit a .env.example with the keys but no values so the next person — or the next deploy — knows what to set. In production, set the same variables through your platform's dashboard or CLI, not a file.

2. Move off the toy database

Generated apps almost always start on SQLite or an in-memory store. That is fine locally and a trap in production: most modern hosts run your app on an ephemeral filesystem, so the file (and every row in it) disappears on the next deploy or restart. You want a managed Postgres instance and a connection string in DATABASE_URL.

The part people skip is migrations. If your schema only exists as "whatever the ORM created on my machine," you cannot reproduce it. Generate a real migration and check it in:

# Prisma
npx prisma migrate dev --name init
npx prisma migrate deploy   # run this in your deploy step

# Drizzle
npx drizzle-kit generate
npx drizzle-kit migrate

Now your database schema is versioned alongside your code, and a fresh environment can be built from zero. This is also the difference between "works on my machine" and an app that keeps working after the first production deploy.

3. Real domain, real TLS

A production app needs a custom domain and a certificate. The good news: certificates are free and automatic almost everywhere now via Let's Encrypt, so this is mostly DNS. You point a record at your host and let it provision the cert:

# Typical DNS records for app.yourdomain.com
# (values come from your host)
CNAME  app     cname.your-host.net.
# apex domains can't use CNAME — use an A / ALIAS record instead
ALIAS  @       your-host.net.

Two gotchas. First, DNS propagation is real — allow up to an hour and verify with dig app.yourdomain.com +short before you panic. Second, once HTTPS works, actually force it: redirect HTTP to HTTPS and set Strict-Transport-Security: max-age=63072000 so browsers refuse to downgrade.

4. Handle errors on purpose

The generated happy path works. The failure path is usually a stack trace rendered straight to the user, which leaks internals and looks broken. You need three things: a catch-all so one bad request cannot take down the process, sane HTTP status codes, and a generic message for users while the details go to your logs.

// Express-style catch-all — the difference between a 500 page and a crash
app.use((err, req, res, next) => {
  req.log?.error({ err, path: req.path }, "unhandled error")
  res.status(err.status || 500).json({
    error: "Something went wrong. Please try again.",
  })
})

// And never let a rejected promise kill the process silently
process.on("unhandledRejection", (reason) => {
  console.error("unhandledRejection", reason)
})

Also set timeouts on any outbound call (payment APIs, the model provider, your own webhooks). An external service hanging for 60 seconds is one of the most common ways a "fast" app becomes unusable under real traffic.

5. Monitoring, so you learn about problems before your users tell you

You cannot fix what you cannot see. The minimum viable setup is three layers:

  • Structured logs shipped somewhere searchable — not just console.log that scrolls off the terminal. Log JSON with a request id so you can trace one user's path.
  • Error tracking (Sentry or similar) that captures the stack, the release, and the user context, then alerts you. Ten lines of setup, enormous payoff.
  • An uptime check that hits a /healthz endpoint every minute and pages you if it fails. Have that endpoint actually verify the database connection, not just return 200.
// A health check that tells the truth
app.get("/healthz", async (req, res) => {
  try {
    await db.query("select 1")
    res.status(200).json({ status: "ok" })
  } catch {
    res.status(503).json({ status: "degraded" })
  }
})

What "done" actually looks like

When all five are in place you can answer, without guessing: Where do my secrets live? Can I rebuild my database from scratch? Does HTTPS work on my domain? What happens on an unhandled error? How would I know if the app went down at 3am? If any answer is "not sure," that is your next task.

None of this is exotic. It is the unglamorous 20% that turns a demo into a product, and it is exactly the part AI code generators leave for you. If you are comfortable with a terminal, an afternoon gets you through the list. If terms like ALIAS records and migration steps are already making you nervous, that is a completely reasonable place to hand it off.

Frequently asked questions

What does it mean to deploy a vibe-coded app to production?

It means taking an app you built by prompting an AI and making it safe and durable for real users: moving secrets into environment variables, using a managed database with versioned migrations, serving it over HTTPS on a custom domain, handling errors gracefully, and adding monitoring so you find out about failures before your users do.

Can I deploy an AI-generated app without knowing DevOps?

For a simple app, yes — modern hosts automate TLS and provide managed databases, so the main work is configuration rather than server administration. The parts that trip people up are database migrations, DNS for custom domains, and error handling. If those are unfamiliar, it is reasonable to have someone set the foundation up once so it is correct.

Why can't I just keep using SQLite in production?

Most hosts run your app on an ephemeral filesystem, so a SQLite file is wiped on every deploy or restart, taking your data with it. A managed Postgres (or MySQL) instance with a connection string in DATABASE_URL keeps data durable and lets multiple instances share it.

How do I add HTTPS to my app's custom domain?

Point a DNS record (CNAME for subdomains, A/ALIAS for apex domains) at your host, and let the platform provision a free Let's Encrypt certificate automatically. Then force HTTP-to-HTTPS redirects and enable HSTS so browsers never downgrade the connection.

Rather have someone handle this end-to-end?

If you'd rather not become an infrastructure engineer to ship your project, we take a GitHub repo and handle the whole deployment — managed for you, or inside your own AWS, GCP, or Azure. No developer needed on your side.

Get your project deployed →