Production Readiness Checklist for AI-Built Apps
Production readiness means closing the gap between an app that works when you use it and an app that holds up when strangers do. Below are thirty checks, each with a way to test it yourself.
Published
This checklist is for anyone who built something with Claude Code, Cursor, Lovable, Replit, Bolt, v0, Windsurf, or Base44 and now has to decide whether it is safe to put in front of real people. Every row has three parts: the check, why it matters, and a specific action you can take to find out where you stand. The verification steps are written for someone who does not write code. Most are done in a browser or in a vendor dashboard.
You can work through all of it without hiring anyone. That is the point of publishing it. A good number of readers will find that everything important already passes, and if that is you, the correct response is to launch. Where a check fails, the fix is often a setting rather than a project.
One note before you start: nothing here proves your app is secure. A checklist finds known problems in the places they usually hide. It cannot prove the absence of others. Treat a clean pass as reassurance, not as a certificate.
If you only do five things
Ranked by what they prevent. If you stop reading after this section, you will still have covered the failures that lose data or money.
- Row-level security is on, and the policies restrict rowsCategory 1. This is the difference between a private database and a public one.
- No service_role key in the browser bundleCategory 2. It bypasses every policy you just wrote.
- Payment webhooks verify their signatureCategory 3. Without it, anyone can tell your app they paid.
- Error monitoring is installedCategory 4. You cannot fix failures you never hear about.
- A spend cap is set on every metered APICategory 5. Five minutes in a dashboard, and it holds when your code does not.
What the research actually shows
Three findings worth knowing before you start, with their limitations attached. The numbers in this niche are frequently wrong, so each one here links to its primary source.
45% of AI-generated code samples failed security testing, introducing OWASP Top 10 vulnerabilities. Syntactic correctness improved sharply from 2023 to 2025 while security pass rates stayed flat at 45–55%.
A review of 43,000+ security advisories traced 74 confirmed CVEs to AI-generated code, including 14 critical and 25 high severity. Roughly 18 appeared across the second half of 2025; 56 appeared in the first quarter of 2026 alone.
AI-assisted developers produced three to four times more code and ten times more security findings. Privilege-escalation paths rose 322% and architectural design flaws 153%, while syntax errors fell 76% and logic bugs fell 60%.
How to read these
Two of the three come from companies that sell security software, which is stated above rather than hidden. None of them says your app is broken. What they support is narrower: the parts of a codebase that concern permissions and data access are where generated code is weakest, so that is where the checking effort belongs. That is why the first two categories below are the ones that matter most.
The thirty checks
Six categories, five checks each. Work top to bottom — the earlier categories carry the larger consequences.
1. Data access and permissions
This is the category that produces the incidents you read about. The question is never whether your interface hides the right buttons. It is whether the database itself refuses a request that comes from the wrong person.
| Check | Why it matters | How to verify it yourself |
|---|---|---|
| Row-level security is enabled on every table that holds user data | With it off, anyone holding your public API key can read the whole table, regardless of what your app's screens allow. | Open your Supabase dashboard, go to Table Editor, and look at the list of tables. Any table showing an 'RLS disabled' or 'Unrestricted' badge is readable by anyone who can reach your API. Write those table names down before you change anything. |
| The policies actually restrict rows, not just permit access | A policy written as 'allow all' satisfies the 'RLS is enabled' check while granting exactly what having it off would grant. | In the Supabase dashboard open Authentication, then Policies. Read each policy's USING expression in plain English. If it says true, or has no condition tying a row to the signed-in user (something like auth.uid() = user_id), it is not restricting anything. |
| A signed-out visitor cannot read another user's records | This is the actual failure that leaks data, and it is testable in about two minutes. | Sign out. Open a private browser window. Take your project's API URL and request a table directly, for example https://YOUR-PROJECT.supabase.co/rest/v1/orders?select=*&apikey=YOUR_ANON_KEY. If rows come back, the rule is not working. An empty array or a permission error is the result you want. |
| A signed-in user cannot read a different user's records | Policies frequently protect against strangers and not against your other customers, which is the harder and more embarrassing failure. | Create two accounts. Sign in as the first and copy the ID from the URL of one of its records. Sign in as the second, paste that ID into the same URL, and reload. You should get a not-found or a permission error, not the first account's data. |
| Admin-only pages are enforced on the server, not by hiding the link | A hidden navigation item is a styling decision. The page is still there for anyone who types the address. | Sign in with a normal, non-admin account. Type your admin URL directly into the address bar, for example /admin or /dashboard/settings. If the page loads, or loads and then flickers away, authorization is happening in the browser and can be bypassed. |
2. Secrets and credentials
Start here by discarding the most common advice in this niche. Not every key that appears in your browser is a leak. One specific kind is, and it is worth knowing which.
| Check | Why it matters | How to verify it yourself |
|---|---|---|
| No service_role or admin key appears anywhere in the browser | The service_role key bypasses row-level security completely. If it reaches a browser bundle, none of your policies matter. | Open your live site, press Ctrl+U (Cmd+Option+U on Mac) to view source, then Ctrl+F and search for service_role. Also open the browser's Network tab, reload the page, and search the loaded JavaScript files for the same string. Any match means rotate that key today, in the Supabase dashboard under Settings, API. |
| Your anon or publishable key is fine where it is | Supabase publishes this key for client use. Treating it as a breach sends people chasing the wrong fix while the real gap stays open. | Confirm the key you found in your bundle starts with the publishable or anon prefix rather than being labelled service_role. If it is the anon key, do nothing to it and go back to the row-level security checks above, which are what make it safe. |
| No secret keys are committed to your repository | Automated scanners crawl public repositories continuously, and git history keeps a file even after you delete it. | On GitHub, open your repository and use the search box at the top with 'in:file' searches for sk_live, SECRET, PASSWORD, and BEGIN PRIVATE KEY. Then check whether a .env file was ever committed by opening https://github.com/YOUR-ORG/YOUR-REPO/commits/main/.env — if that page shows commits, the values in it must be rotated. |
| Your repository is private if it was ever meant to be | Builder tools and AI agents create repositories on your behalf, and the visibility setting is not always what you assumed. | Sign out of GitHub or open a private window, and paste your repository URL. If the code loads without a login, it is public. Then check Settings, then Secret scanning, and read any alerts already sitting there. |
| Every third-party key is scoped and restricted | A key that can do anything turns a small leak into a large one, and most providers let you narrow it in a few clicks. | Sign in to each provider you use — Stripe, OpenAI, Anthropic, Google Maps, SendGrid — and open its API keys page. Look for a restrictions or permissions setting on each key. A Stripe key labelled 'Standard keys' with full access, or a Google Maps key with no HTTP referrer restriction, is one to narrow now. |
Across roughly 20,000 repositories with GitHub Copilot activity, 6.4% leaked at least one secret, compared with a 4.6% baseline — a 40% relative increase.
3. Payments and money
Payment code demos well because the happy path is easy. The money is lost on the other paths: the forged callback, the double-click, the price the browser was allowed to choose.
| Check | Why it matters | How to verify it yourself |
|---|---|---|
| Webhook signatures are verified | Without verification, anyone who knows your webhook URL can send a fake 'payment succeeded' message and receive whatever you grant paying customers. | Search your codebase for constructEvent or the equivalent verification call in your payment library — in most editors that is Ctrl+Shift+F for a project-wide search. If your webhook handler reads the request body and acts on it without a verification step and a signing secret, it is unverified. |
| Prices come from your server, not the browser | If the amount is submitted by the page, someone can edit it before it is sent and pay what they choose. | Open your checkout page, press F12 for developer tools, go to the Network tab, and start a purchase. Click the checkout request and look at the payload. If it contains a price or amount field, that number came from the browser and can be changed. |
| Failed and expired payments are handled explicitly | Declined cards and lapsed subscriptions are routine. If nothing handles them, customers keep access without paying or lose access without warning. | In Stripe test mode, use card number 4000 0000 0000 0341, which attaches successfully then fails on charge. Complete a signup with it and watch what your app does. A blank screen, a spinner that never resolves, or an account that gets full access is a gap. |
| Submitting twice does not charge twice | Impatient users double-click, and networks retry. Both produce duplicate charges when nothing guards against them. | In test mode, click your pay button twice in quick succession, then open the Stripe dashboard Payments list. Two payment records for one order means there is no duplicate guard. |
| Refunds, cancellations, and downgrades actually change access | The billing system and your app can disagree, leaving people paying for nothing or using something they cancelled. | In test mode, subscribe, then cancel from the Stripe dashboard, then reload your app as that user. If the paid features are still available a few minutes later, your app is not listening to cancellation events. |
4. Reliability and errors
Generated code tends to assume every request succeeds. Real networks disagree. The goal here is that failures are visible to you and survivable for the user.
| Check | Why it matters | How to verify it yourself |
|---|---|---|
| You find out about errors from a dashboard, not a customer | Server errors are invisible from the outside. Without monitoring, the first report comes from whoever gave up on your product. | Ask yourself where you would look right now to see what broke in the last hour. If there is no answer, install error monitoring — Sentry has a free tier and installs in under an hour. Then trigger a deliberate error and confirm the email arrives. |
| A failed request shows a message, not a blank screen | A white page reads as a broken product, and the user has no idea whether their action went through. | Open your app, press F12, go to the Network tab, and switch the throttling dropdown to Offline. Now click through your main flow. Every action should say something. Anything that shows an endless spinner or an empty page needs handling. |
| Form input is validated on the server, not only in the browser | Browser validation is a convenience for honest users. It is trivially skipped by anyone calling your API directly. | In developer tools, open the Console and remove an attribute: document.querySelector('input[required]').removeAttribute('required'). Submit the empty form. If it saves a blank or malformed record, nothing is checking on the server. |
| Sessions expire and signing out really signs you out | Shared and stolen sessions are a common route into an account, and generated auth flows often skip expiry entirely. | Sign in, then sign out, then press your browser's back button. If the previous page still shows your data, or a page reload restores the session, sign-out is only clearing the screen. |
| Every dependency you install exists and is the package you meant | AI tools invent package names, and attackers register the invented ones. Installing one runs their code on your machine and your server. | Run npm audit in your project folder and read the output. Then open package.json and search npmjs.com for any dependency name you do not recognise. A package with almost no downloads, no repository link, or a first publish date days after you were told about it deserves a second look. |
Across 2.23 million generated code samples, 19.7% referenced a package that does not exist, producing 205,474 unique fabricated package names. 43% of hallucinations repeat across runs.
5. Performance and cost
Most apps do not have a scaling problem. They have a few slow queries and one metered API with no ceiling. Both are cheap to find before customers do.
| Check | Why it matters | How to verify it yourself |
|---|---|---|
| Your common queries have indexes | A query that is instant against fifty rows can take seconds against fifty thousand, and the change arrives suddenly. | In the Supabase dashboard open Advisors, then Performance. It lists tables with missing indexes and unindexed foreign keys directly. Anything flagged there is a specific, named fix rather than a vague performance concern. |
| List pages do not fetch the whole table | Loading every row to display ten is fine at launch and unusable at volume, on both speed and bandwidth. | Open your busiest list page with developer tools on the Network tab. Click the data request and look at the response size and the number of records returned. If it returns everything and the page shows a page-worth, add pagination. |
| Anything metered has a hard spend cap set at the provider | A retry loop calling a paid model can run all night. The provider's own cap is the only limit that holds when your code misbehaves. | Sign in to each paid API provider and find its usage limits page — for Anthropic and OpenAI this is under billing or usage limits. Set a monthly maximum and an email alert threshold. If no number is set, the ceiling is your card limit. |
| Expensive endpoints are rate limited per user | One user, or one script, can otherwise consume the budget for everyone. | Find your most expensive action — usually the one calling an AI model or sending email. Click it fifteen times in a row as fast as you can. If all fifteen go through, there is no limit, and a script would do it fifteen thousand times. |
| File uploads have a size and type limit enforced server-side | Without one, storage costs and processing time are set by whoever uploads the largest file. | Create a large dummy file (on macOS: mkfile 500m big.bin in Terminal) and try uploading it through your app. If it accepts a 500 MB file, or accepts a file whose extension you renamed to .jpg, the limits are not being applied where they count. |
6. Operations and recovery
This category is about the day something goes wrong. Every item is a question with a yes-or-no answer, and finding out now costs nothing.
| Check | Why it matters | How to verify it yourself |
|---|---|---|
| Your code is in a repository you control | Code living only inside a hosted builder can be lost with an account, and cannot be reviewed, deployed, or handed to anyone else. | Open github.com (or your git host) and confirm you can see the current version of your app under an account you own. If the only copy is inside Lovable, Replit, Bolt, v0, or Base44, use that tool's export or GitHub-sync option now. |
| Backups exist and have actually been restored once | A backup nobody has restored is a belief, not a recovery plan. Restoration is where the surprises are. | In the Supabase dashboard open Database, then Backups, and confirm you see recent entries. Then restore one into a new, separate project and open it. If you cannot complete that in an afternoon, you do not yet have a tested backup. |
| Development and production use different databases | Sharing one database means a mistake while experimenting hits live customer data. | Compare the database URL in your local .env file with the one in your hosting provider's environment variables (Vercel: Settings, Environment Variables). If they are the same string, your test data and your customers' data are in the same place. |
| You can undo a bad deploy in minutes | The fastest fix for a broken release is going back to the previous one, and that only works if it is set up beforehand. | In Vercel or Netlify, open Deployments, find the previous successful deploy, and confirm you can see a Rollback or 'Promote to Production' option. Click through to the confirmation dialog once, without confirming, so you know where it is. |
| AI agents cannot reach production on their own | An agent with production credentials in its environment can act on them, including destructively, without asking first. | Open the .env files and MCP or tool configuration your coding assistant can read, and look for production database URLs, service_role keys, or live payment keys. Anything the agent can read, it can use. Move those values out of its reach and keep them in your hosting provider's environment settings. |
What a passing score means, and what it does not
Worth being precise about, because the alternative is selling you anxiety.
If all thirty pass, you have closed the gaps that account for the incidents people write about: open databases, keys where they should not be, forged payment callbacks, unbounded spend, and no way back from a bad deploy. That is a reasonable position to launch from.
It does not mean your app is secure. No checklist can establish that, and neither can a scanner. Automated tools confirm that a policy exists; they cannot tell you whether the rule that lets a user read their own records also quietly lets them read everyone else’s. That kind of logic error is the common one in generated code, and finding it requires someone to read the code.
It also does not cover compliance. HIPAA, SOC 2, and PCI-DSS have their own requirements and their own assessors, and passing this list is not evidence toward any of them.
When to bring in someone else
Three situations genuinely warrant help. The first is several failures in categories 1 and 3 at once, because that usually indicates the app trusts the browser to enforce rules and the pattern will repeat everywhere. The second is a check you cannot verify — if you run the test and cannot tell what the result means, guessing is worse than asking. The third is a fix you applied that you cannot confirm worked.
Everything else can wait. If you are not storing personal data, not taking payments, and not carrying users who would be harmed by an outage, the honest advice is to set a spend cap, push your code to a repository you own, and go get customers. You can come back to this page when you have them.
If you want a second opinion on what you found, our codebase review is the fixed-price version of this list done properly, and the five-minute Supabase check goes deeper on the first category alone.
Questions about this checklist
Do I need to complete all thirty checks before launching?
No. The five checks listed at the top of this page cover the failures that lose data or money, and most apps can launch once those pass. The rest are worth working through in the weeks after launch, in the order the categories appear.
Is it dangerous that my Supabase anon key is visible in my page source?
No, that key is designed to be visible in the browser and Supabase documents it as safe to expose, provided row-level security is enabled on every table it can reach. The keys that must never appear in a browser are service_role, Stripe secret keys, and database connection strings. If someone offers to sell you a fix because your anon key is public, they have not read the documentation.
Can I do these checks myself if I do not write code?
Most of them, yes — the verification steps are deliberately written as things you do in a browser or a vendor dashboard rather than in an editor. A handful ask you to search your codebase or run one command, and you can paste those instructions into the AI tool you built the app with and have it run them.
What if a check fails? Do I need to hire someone?
Usually not immediately. A failing check tells you what to fix, and several fixes are a dashboard setting: spend caps, key restrictions, environment separation, rollback. Writing correct row-level security policies for a small schema is often a day of work. Hiring makes sense when several checks in the data-access or payments categories fail at once, or when you cannot tell whether a fix worked.
My app has a handful of users and no sensitive data. Does any of this apply?
Less of it than you might fear, and you are probably fine. If you store no personal data, take no payments, and nobody depends on the app being up, the honest advice is to set a spend cap, get the code into a repository you own, and stop worrying about the rest until real users or real data arrive.
Ran the checks and not sure what you found?
Send us the results, or the two that worried you. If the honest answer is that your app is fine and you should launch, that is what you will get back.