Production Readiness Guide

Is your AI-built app
actually ready for real users?

Apps built with Lovable, Bolt, Replit, Cursor, and similar tools ship fast. That's the point. But fast-shipped apps share a predictable set of gaps — and most of those gaps are invisible until a real user triggers them. This guide covers the most common ones.

The honest answer

Probably not, if you haven't specifically checked. That's not an insult — it's structural. AI code generators optimize for making things work in the happy path. They scaffold a login flow that logs in. They create a database schema that stores data. They wire up a payment form that charges a card.

What they don't do well: configure the security boundaries, think about what happens when something fails, or worry about what happens when your third user opens the app at the same time as your first two.

The risks aren't theoretical. They are specific, common, and fixable — if you know they're there.

The most common gaps

Ordered by how often we find them in free audits, not by severity.

Row-Level Security not configured

CriticalLovableBoltReplit

Supabase enables RLS per table, but it defaults to off. AI-generated code often creates the tables and skips the policies. The result: any authenticated user can read every row in your database.

How to check yours

In the Supabase SQL editor, run: select tablename, rowsecurity from pg_tables where schemaname = 'public'; Every table that comes back with rowsecurity = false and holds user data is readable by anyone with your anon key — which ships in your front end. Enabling RLS is only half of it: a table with RLS on and no policies denies everything, so check pg_policies for coverage too.

More on this in the lovable teardown →

API keys exposed in client bundles

CriticalCursorv0Lovable

If a secret key is referenced in a React component or anywhere in your frontend code, it ships to the browser. Every visitor can extract it from devtools. This includes Stripe keys, OpenAI keys, and internal service credentials.

How to check yours

Run your production build, then grep the output for recognizable prefixes: sk_live and sk_test for Stripe, service_role for a Supabase service key, eyJ for any JWT. Anything that turns up in dist/, .next/, or build/ has been public for as long as that build has been deployed — move it server-side AND rotate it, because moving it alone does not un-publish it.

More on this in the bolt teardown →

No rate limiting on auth endpoints

HighAll

Password reset, magic link, and sign-up endpoints with no rate limit are trivially brute-forced or used to spam your users. Most AI-generated auth scaffolding does not include rate limiting by default.

How to check yours

Send a burst at one endpoint and count the status codes — a hundred requests in a shell loop, piped through sort and uniq -c. All 200s means nothing is limiting you. On Supabase, also read Authentication → Rate Limits: defaults exist, but they are project-wide and sized for a busy app, so tighten them to what one legitimate user could plausibly need in an hour.

More on this in the replit teardown →

Public storage buckets

HighLovableBoltSupabase-based apps

A public Supabase storage bucket serves every object in it to anyone holding the URL, with no auth check at all. User-uploaded files — profile photos, documents, receipts — are on the open web unless the bucket is private and files are served through signed URLs.

How to check yours

Run select id, name, public from storage.buckets; and treat every public bucket holding user uploads as already exposed. Then confirm it: copy one object URL and open it in a private window with no session. If it renders for you, it renders for everyone. The fix is a private bucket plus short-expiry signed URLs generated server-side.

More on this in the lovable teardown →

Missing database indexes

MediumAll

Postgres indexes primary keys automatically and foreign keys never. AI code generators create tables and queries but rarely add the missing indexes, so a query that returns instantly at 100 rows scans the whole table at 100,000.

How to check yours

Take your slowest page's query and run it with EXPLAIN ANALYZE. A Seq Scan on a table that will grow is the finding. Then check the schema more broadly: list foreign-key columns and confirm each has an index behind it. Worth doing before launch rather than after, because the symptom only appears at a data volume you cannot easily simulate later.

More on this in the lovable teardown →

Stripe webhooks without signature verification

HighCursorv0

Stripe signs every webhook it sends. A handler that calls JSON.parse on the body instead of stripe.webhooks.constructEvent skips verification entirely and cannot tell a real event from one anybody POSTed — including a checkout.session.completed that unlocks paid features for free.

How to check yours

Grep the handler for constructEvent and confirm it reads the RAW body — verification fails against a body a framework already parsed and re-serialized. Then prove it: POST a hand-written checkout.session.completed payload to the endpoint with no signature header. A correct handler answers 400. Anything else means unsigned events are being trusted.

More on this in the cursor teardown →

No error boundaries or fallback states

MediumAll

One thrown error in one component unmounts the entire React tree. The user gets a blank white document with no explanation and no way back — and because nothing was logged, you get no signal it happened at all.

How to check yours

Check that every route has a boundary above it: error.tsx in a Next.js App Router segment, errorElement on a React Router route, or a class component with componentDidCatch. Then prove it works — throw on purpose inside a leaf component and confirm you get a fallback UI instead of a blank page.

More on this in the bolt teardown →

Quick self-check

Work through this before you share your app publicly. Items marked critical are the ones we see cause actual incidents.

Supabase RLS enabled on every table that contains user datacritical

No secret keys referenced in frontend codecritical

Stripe webhook endpoint verifies Stripe-Signature headercritical

Storage buckets are private; user files served via signed URLscritical

Auth endpoints (login, password reset, sign-up) have rate limitingcritical

Database indexes exist on foreign keys and frequently queried columns

Error boundaries at route level — users see an error page, not a blank screen

Environment variables used for all secrets, not hardcoded stringscritical

CORS configured to reject requests from unknown origins

User-facing inputs are sanitized before database writes

Go deeper by platform

The gaps above are the common set. Each builder also has its own defaults worth checking specifically.

Frequently asked

Is my AI-built app production ready?

Probably not without a specific check. AI code generators optimize for the happy path — they scaffold auth flows and database schemas, but skip security boundaries, concurrency handling, and failure paths. Work the checklist below; if you would rather someone else did, the free audit reports the same findings against your actual repo.

Is my Lovable app secure?

Lovable generates React + Supabase apps fast, but row-level security is off per table until someone enables it and writes a policy, and storage buckets are public until switched to private. Neither is part of generating a working feature, so both are commonly missing. The Lovable teardown covers the specific checks.

What is a Supabase RLS check?

Row-Level Security is a Postgres feature that controls which rows each user can read and write, and it is off per table by default. An RLS check verifies that policies exist and are correctly scoped on every table holding user data. Run: select tablename, rowsecurity from pg_tables where schemaname = 'public';

How do I know if my vibe-coded app is safe?

Have someone who did not build it review it against a specific checklist, with real commands rather than a mental model of what should be there. Every item in this guide includes how to verify it yourself. The free audit at turtlecreekllc.com/request-audit runs the same checks against your repo and returns a severity-ranked report.

Can I check these things myself, or do I need a tool?

Almost all of them are a SQL query, a grep over your build output, or a curl against your deployed URL — no tooling beyond what you already have. The value of an audit is coverage and consistency: running every check on every route, every time, rather than the three you remembered.

Which of these should I fix first?

The ones that expose data to people who never had an account: row-level security, public storage buckets, secrets that reached the client bundle, and unverified payment webhooks. Everything else costs you performance, support tickets, or a bad night — those four cost you other people's data.

Does this apply if I used Cursor or Copilot rather than a no-code builder?

Yes, though the failure modes shift. Prompt-to-app tools tend to miss configuration boundaries; AI pair-programmers tend to miss verification steps inside otherwise correct code — an unverified webhook signature, an auth middleware applied to most routes but not all, a read-modify-write with no transaction. The Cursor teardown covers that set.

What to do if you're not sure

The most reliable approach is to have someone who didn't build the app look at it with a specific checklist and real tools — not just a mental model of what “should” be there.

That's what the free audit is. Submit your deployed URL and a few qualifying questions. Shane reviews the app with AI-assisted scanning and human judgment, then sends back a written report with specific findings — not generic advice.

If everything looks fine, you'll know it looks fine. If something is wrong, you'll know exactly what and why.

Get your free audit

Five questions. Written report within 48 hours. Personally reviewed by Shane Jordan. Free, no obligation.

Request Free Audit →
Operational IntelligenceOur newsletter on operational AI, in your inbox.
Subscribe free →