"It works. I just don't know if it's safe to launch."
This is the sentence I hear most often from people who built an app with Claude Code, Cursor or Lovable. The instinct is correct. The reason, though, is probably not the one you have in mind.
The problem isn't "AI wrote the code, so it can't be trusted." It is simply this: AI implements the features it is asked for, and does not implement the defences it isn't. And someone building an app for the first time doesn't know what to ask for. Of course they don't — if they did, they could have written it themselves.
This guide lists the things that go unasked, ordered by what can actually hurt you. Every technical term is translated into plain language the first time it appears. Each item comes with a way to check it yourself and an instruction you can paste straight into Claude Code or Cursor.
The conclusion up front: there are four items you must close before launch. The rest can wait.
There is a free self-check that turns this guide into 20 questions. No signup, and nothing you type leaves your browser. If you'd rather know where you stand before reading, start there.
1. Why "it works" and "it's safe" come apart — the structural reason
First, let's establish with measurements that this isn't just you.
Veracode tested more than 100 large language models across 80 coding tasks and found that 45% of cases introduced an OWASP Top 10 vulnerability. Defences against cross-site scripting — where a script planted by someone else runs on your own site — failed in 86% of samples.
GitGuardian's 2026 study found that commits written with AI assistance leaked API keys and passwords at roughly twice the rate of human-only commits.
This is not the kind of problem a smarter model quietly removes. The reason is simple: AI targets "does it run", not "can it be attacked."
To put it in cooking terms: AI is extremely good at making the dish taste good, and doesn't think about food poisoning unless told to. And you cannot detect food poisoning by tasting.
The opposite of "it works" is not "it's broken"
This is the part first-time builders find counterintuitive.
- works / doesn't work — you can determine this by trying it
- safe / unsafe — you cannot determine this by trying it
An app where other people's data is visible works flawlessly for as long as you use it with your own account. It never once looks broken. Not until someone changes one digit in a URL.
So before launch, you have to deliberately try the bad things. That is what this guide is.
2. The order of danger — what to close first
Listing everything at once is demoralising, so let's fix the order. The criterion is whether the damage can be undone.
| Rank | Item | What happens | Recoverable? |
|---|---|---|---|
| 1 | No database permissions (RLS) | Anyone can read and write every user's data | No (leaked data doesn't come back) |
| 2 | Secret keys exposed to the browser | Same as above, plus deleting and tampering | No |
| 3 | No server-side ownership check | Changing a number in the URL reveals someone else's data | No |
| 4 | Duplicate payment processing not prevented | Customers charged twice; goods granted twice | Refundable, but trust isn't |
| 5 | Legal gaps (personal data, commercial disclosures) | Non-compliance; payment provider review fails | Fixable |
| 6 | No cap on usage-based billing | A bill you didn't expect | Negotiable, painful |
| 7 | No backup or recovery path | You can't get back to a working state | Preparation is the only defence |
| 8 | No visibility into errors | Broken for months without anyone noticing | Fixable once noticed |
Items 1–4 are the "before launch, without exception" set. Items 5–8 are fine to handle right after launch.
Let's take them in order.
3. [Highest priority] Database permissions — what RLS actually is
What this is about
RLS (Row Level Security) is the database-side setting that decides who can see which rows.
Think of a "row" as one row in a spreadsheet. Say you have a table of users. RLS is what makes A able to see only A's row, and B only B's.
Why it's fatal
With a service like Supabase, queries go from the browser directly to the database. The anon (anonymous) key used for this is public by design. Open the browser developer tools and anyone can read it — and that is the normal, intended state.
The problem is that without RLS, that public key alone is enough to read and write all your data.
Which plays out like this:
- An attacker opens your app
- They read the database URL and
anonkey from the developer tools (both public information) - They query the database directly
- Every table, for every user, comes back
Having a login screen is irrelevant. The login screen is a browser-side concern, and the database doesn't know about it.
This has actually happened
The vulnerability registered as CVE-2025-48757 is described as:
An insufficient database Row-Level Security policy in generated sites allows remote unauthenticated attackers to read or write to arbitrary database tables
Severity: CVSS 9.3 CRITICAL. That is the highest band. The affected product was the AI app builder Lovable.
What matters here is that the vendor has disputed this CVE, and why. Their position was that protecting application data is the customer's responsibility.
Which means — the responsibility for protecting the data in the app you built is officially yours. Do not expect the tool to cover it for you.
How to check
If you're on Supabase:
- Open the Supabase dashboard → Table Editor
- Look for warnings on the table list such as "RLS disabled" or "Unrestricted"
- Any table showing one is fully public
RLS being enabled isn't enough either. If the policy (the condition that grants access) is USING (true), it means "everyone can see every row" — identical in effect to having it off.
The instruction to paste into your AI tool
Read every Supabase migration/SQL file and report the following as a table.
(1) Tables where RLS is not enabled
(2) Tables where RLS is enabled but no policy exists
(3) Tables whose policy condition effectively allows everyone, such as USING (true)
Then, for each table, write the RLS policy SQL that scopes rows to "only the
logged-in owner". For performance, write auth.uid() in the form
(select auth.uid()).
That last point, (select auth.uid()), is the standard performance idiom. Without it, the database can slow down dramatically as the number of rows grows.
For more detail, see What is Supabase "RLS"? — how AI-built apps end up with all their data public. To check whether the policy you wrote actually scopes anything, paste it into the free RLS checker.
4. [Highest priority] Secret keys — why the "API key" AI wrote is visible to others
What this is about
Apps hold "keys" so they can use external services: databases, payments, AI. Put a key in the wrong place and it leaves with anyone who visits.
The most common mistake is adding NEXT_PUBLIC_ to a value that must never have it.
In Next.js, when an environment variable's name starts with NEXT_PUBLIC_, its value is inlined into browser-facing files at build time. This is documented, intended behaviour, not a bug — it exists for values that genuinely need to run in the browser.
The problem is that AI, trying to make things run, sometimes applies that prefix to a secret key. It does make it run. It also makes it readable by anyone.
The keys that matter most
Supabase issues two kinds of key:
anonkey — public by design. Subject to RLS. Fine in the browser.service_rolekey — a key that bypasses every restriction. Goes straight past RLS. Must never be in the browser.
If the service_role key leaks, configuring RLS correctly buys you nothing. Everything is bypassed. AI-generated code reaches for this key because "it just works" — you can fetch data without writing a single RLS policy, so no error appears.
How to check
- Search your project for
NEXT_PUBLIC_ - For each value that comes back, ask: "would I be fine with a stranger having this?"
- If you hesitate at all, it's a value that needs to move
Also, if your code is on GitHub, check whether a .env file has been committed. A key you commit once stays in the history even after you delete the file. Deleting is not a fix — the key itself has to be reissued.
The instruction to paste into your AI tool
Search this entire project and check whether any secrets are stored in
environment variables prefixed with NEXT_PUBLIC_ (Supabase service_role key,
database passwords, secret keys for any API).
If you find any, move the value to a server-only environment variable and move
the code that reads it to the server side (Route Handler / Server Action).
List every place a client component references a secret.
Report only which file and line holds which kind of key — never print the
secret values themselves.
The details are in
NEXT_PUBLIC_andservice_role— why the "API key" AI wrote is visible to others.
5. [Highest priority] Ownership checks — "looks logged in" versus real permissions
What this is about
This one is hard to explain, and the most frequently missed.
Suppose your app has a screen at /orders/123, where 123 is an order number. Now, what happens if you change 123 to 124?
If someone else's order appears, that is a vulnerability. OWASP — the standard body for web security — ranks it number one among API risks, the most common hole there is.
Why it happens
Because it feels safe if there's no link to other people's data on screen. But typing the URL directly still sends the request. Whether a link exists is irrelevant.
Asked to "show a list of the logged-in user's orders," AI does filter the list properly. Asked to "show the order detail," it tends to write code that just looks up the number. And because the list is correctly filtered, you will never notice while testing normally.
How to check — a browser is all you need
- Create two test accounts (A and B)
- Log in as A and create some data. Note the number (or ID) in the URL
- Log out and log in as B
- Type the noted URL straight into the address bar
If B can see A's data, that's a failure. If you get an error or "you don't have permission," it's working.
Anyone can do this within their own app, so do it once before launch.
The instruction to paste into your AI tool
Find every handler in this app that takes an ID and returns or updates data
(Route Handlers, Server Actions, APIs), and check one by one whether the server
verifies that the record belongs to the logged-in user.
Fix any that don't by adding an ownership check. Also report any place where
access is merely hidden in the frontend, since that is not a defence.
If RLS is configured correctly, this defence becomes two layers deep. Put another way: RLS and server-side checks are both required, not either/or.
Is the login AI built for you real? — "looks logged in" versus real authorization covers this in depth with worked examples.
6. [Highest priority] Payments — why double-charging happens
If your app has no payments, skip this section.
What this is about
Stripe and other payment providers send your app a notification when a payment completes — a webhook. Your app receives it and hands over the goods, or increases a balance.
Here is the trap. These notifications are delivered "at least once", which means the same one can arrive multiple times. If the network hiccups, or your app is slow to respond, the provider decides it may not have arrived and resends. This is by design, and Stripe's official documentation says so.
An implementation that adds to a balance on every notification received will grant twice.
Why AI misses it
Ask for "increase the balance when payment completes" and AI writes exactly that. It's a correct implementation. What it doesn't consider is "what if the same notification arrives twice" — because nobody asked.
And this defect never appears in testing. Locally the notification arrives once. It shows up in production, during your busiest hour.
The fix — idempotency
The countermeasure has a name: idempotency — the property of producing the same result however many times you do it.
Concretely: store the event ID from the notification in the database, and when an already-processed ID arrives, do nothing and simply reply "received." It is not difficult. It is just forgotten.
On the payment platform I worked on, this design has kept double-charges in production at zero. This is not a technically advanced topic — the difference is only whether you decided to always do it.
The instruction to paste into your AI tool
Read this app's payment webhook handler and check whether the same event
arriving multiple times can be processed twice.
Add idempotency: store the event ID as a unique key and return early if it has
already been processed. Keep the record and the business change in a single
transaction, and return a status code that lets the provider retry on failure.
Also check (1) whether the amount uses a value sent from the client, and
(2) whether the webhook signature is verified.
Why double-charging happens when you let AI build payments, and how to prevent it also covers amount tampering and signature verification.
7. Code that fails quietly — it runs, but it's wrong
From here on these aren't "before launch, without exception," but they bite if left alone.
The most common defect in AI-generated code is not, in fact, a vulnerability. It is errors in error handling and business logic. What makes them awkward is that nothing errors.
- Totals are slightly off (a boundary date isn't included)
- Search results drop entries (order is non-deterministic, so pagination loses rows)
- Something failed but the screen says it succeeded (the exception was swallowed)
All of these look normal on screen. The numbers are just a little wrong. So nobody notices for months.
The countermeasure is to state the conditions that must never break, up front. "The total always equals the sum of the line items." "The same search run twice returns the same order." Write down obvious things like these and have the AI turn them into tests.
Covered in "It runs, but it's wrong" — the quietest and most common AI-generated bug.
8. Money and law — two easily missed items
Usage-based billing
Cloud services and AI APIs bill for what you use. With unexpected traffic or an accidental loop, the bill spikes easily. Surprise bills in the thousands are not rare for solo builders.
Three things to do right after launch: turn on billing alerts for each service, set a per-key spending limit on AI APIs, and add rate limiting in the app — a mechanism that caps repeated requests from the same visitor.
The bill spiked after launch — usage-billing incidents and how to cap them
Legal
If you hold even one email address, publishing a privacy policy is an obligation under Japan's personal data protection law. "I'm an individual" and "it's free" are not exemptions.
If you charge, you also need the commercial-transaction disclosures — in Japan, the notice required by the Specified Commercial Transactions Act. The seller, contact details, price, payment timing, and refund terms are all required by law. Stripe's onboarding review can also fail if this is missing.
9. Can you get back? — the operational minimum
Finally, three things that matter after launch.
Backups. Asking an AI to "tidy up this table" can delete data you didn't mean to lose. Free tiers often have short retention, or no automatic backups at all. "By the time I noticed, the recovery window had closed" does happen.
Rollback. When a deploy breaks things, do you know how to get back to the previous version? Hours of downtime in the middle of the night is preventable by trying the procedure once. Note that database schema changes, unlike code, do not roll back automatically.
Error visibility. The most common failure for solo builders is not noticing. A contact form broken for months genuinely happens. Errors that fail in the browser never reach your server logs at all. Adding one error-monitoring service with a free tier changes the picture substantially.
If you're thinking as far as handover, see Preparing to consult or hand over to an engineer.
10. Pre-launch checklist (printable)
Top to bottom, one at a time.
Before launch, without exception
- RLS is enabled on every table, with policies scoped to "the owner's rows only"
- Writes (INSERT/UPDATE) are restricted too (
WITH CHECK) - No
service_rolekey or database password sits in aNEXT_PUBLIC_variable -
.envis not committed to GitHub - Any key that was ever visible to others has been reissued
- Verified with a second account that changing the URL number does not expose another user's data
- Admin features verify permissions on the server
- (If you have payments) A duplicate webhook cannot be processed twice
- (If you have payments) The amount is decided on the server
- (If you have payments) The webhook signature is verified
Right after launch
- Published a privacy policy
- (If paid) Published the commercial-transaction disclosures
- Set billing alerts and caps on every service
- Confirmed whether backups exist and their retention period
- Tried the rollback procedure once
- Added error monitoring, or made failures of critical operations reach you
- Confirmed on the production URL that signup and password reset actually work
There's a free self-check that turns this checklist into 20 questions, orders the result by severity, and hands you fix instructions to paste into your AI tool. No signup, about five minutes.
Summary — building it this way was not the mistake
One last thing.
Building an app with generative AI was entirely the right call. I write production products with Claude Code every day. It is genuinely fast and cheap, and there is no reason to avoid it.
But shipping fast and shipping safely are two different jobs. AI got dramatically better at the first. The second is still ours.
And the second takes less time than you'd think. Closing the four items in this guide — RLS, exposed keys, ownership checks, payment idempotency — prevents nearly every catastrophic incident. You don't have to rebuild anything.
If an item is unclear, start by pasting that section's instruction into your AI tool. Bring in a human only for what's left — particularly authorization design, payments, and handling personal data.