Your AI wrote the code. Nobody wrote the policies.
Lovable, Bolt.new, Replit, Claude Code and Cursor all write the same app in a different accent, and they get the same five things wrong. Not because the generated code is bad — it is usually fine — but because security lives in configuration nobody prompted for. Here is what to check, in the order that finds the worst thing fastest.
Apps built with AI tools fail in configuration rather than in code. The five recurring failures are secret API keys compiled into the browser bundle, Row Level Security never enabled on a table, authorisation enforced only by hiding buttons in the interface, storage buckets left public, and coding agents given production database credentials.
None of these five failures is visible from inside the running app, because the person testing it is signed in as themselves and sees exactly what they expect. Auditing an AI-built app therefore means querying the database and searching the compiled bundle directly, not clicking through the interface.
The preview is the problem
An AI builder optimises for one signal: does the thing you asked for appear and work. That signal is honest about layout, routing and state, and completely blind to authorisation. A table with no access policy renders your dashboard perfectly. So does a table anyone on the internet can download.
This is why these failures reach production. You are not ignoring a warning — there isn't one. You are signed in as yourself, so every query you make legitimately succeeds, and the query someone else could make never gets run in front of you. The app is not lying to you. It is answering a different question.
Which means the audit cannot happen in the browser. Every check below reads the database or the compiled output directly.
The five failures
1. Secret keys compiled into the bundle
Frontend build tools publish environment variables by prefix — VITE_ in Vite, NEXT_PUBLIC_ in Next.js — and publishing means substituting the value into a JavaScript file you serve to the public. That is correct for a Supabase anon key, which is designed to be public. Give the same prefix to an OpenAI key, a Stripe secret key, or the Supabase service_role key and it ships to every visitor, readable with view-source. The service_role key is the severe one: it bypasses every access policy in your database by design.
Nothing errors, and the app works better with the key there — which is usually how it got added. Detail on the build-tool mechanics is in the Bolt.new guide.
2. Row Level Security never enabled
Supabase exposes your tables over a public HTTPS API. Row Level Security is the only thing standing between that API and every row you have, and creating a table does not turn it on. A table without it is not “protected by the app” — it is readable by anyone holding the anon key, which is in your bundle on purpose.
Enabled is also not the same as correct. A policy of USING (true) is switched on, attached, and returns every row to everybody. See the Row Level Security guide for how a broken read policy returns an empty list instead of an error.
3. Authorisation that only hides buttons
Ask for an admin area and you reliably get one: a check on the user's role that renders the admin panel or doesn't. That is a rendering decision made in the browser, on data the browser is holding, and it can be flipped in the developer console in about four seconds. If the underlying table has no policy of its own, the data was always available — the interface was just declining to show it.
The rule is that the database has to enforce it. Anything the frontend decides is a convenience, not a control.
4. Storage buckets left public
A public Supabase bucket serves any object to anyone with the URL, with no authentication. That is the right setting for avatars and marketing images, and it gets applied to invoices, passports and uploaded documents because public buckets are the configuration where uploads work first time. Object URLs are guessable enough, and leak through referrer headers and shared links besides.
5. Agents holding production credentials
Connecting a coding agent to your live database gives a system that acts on natural language the ability to run destructive SQL. A Replit agent did exactly that in 2025 — dropped a production database during a code freeze, then reported success. Worse, an agent that reads your database is reading text your users wrote, which means prompt injection becomes a database threat. Point agents at a development branch; see the Replit guide for what production access actually costs.
Audit your app in thirty minutes
Run these in order — the first one finds the highest-severity problem, and each is a single command or query against your real project rather than an inspection of your source.
If you would rather have your agent do it: the audit prompt wraps steps 2 to 5 below into one read-only block you can paste into Claude Code or Cursor.
Step 2 — which tables have no Row Level Security?
In the Supabase SQL editor. Every row this returns is a public table:
select tablename
from pg_tables
where schemaname = 'public'
and rowsecurity = false;Step 3 — which tables have it enabled but no policy?
These are the reverse failure: protection is on, no policy grants access, and the table silently returns nothing to your own app. Harmless for security, but it is usually the reason a feature “stopped working” and someone is about to disable Row Level Security to fix it.
select t.tablename
from pg_tables t
left join pg_policies p
on p.schemaname = t.schemaname and p.tablename = t.tablename
where t.schemaname = 'public'
and t.rowsecurity
and p.policyname is null;Step 4 — which policies let everyone through?
A policy whose condition is literally true passes every audit that only asks whether Row Level Security is enabled, and returns every row to every caller:
select tablename, policyname, cmd, qual
from pg_policies
where schemaname = 'public'
and qual = 'true';
-- Policies that accept any new row: a user may edit a row they own
-- and reassign it to someone else.
select tablename, policyname, cmd
from pg_policies
where schemaname = 'public'
and with_check = 'true';Note the second query looks for WITH CHECK (true), not for a missing WITH CHECK. Omitting the clause is safe: Postgres reuses the USING expression to validate the updated row, so a user cannot hand their row to somebody else. Writing WITH CHECK (true)explicitly is what disables that, and it is a common way to clear an “violates row-level security policy” error without understanding it.
Neither query catches the subtler one: a user updating their own row to set role = 'admin' satisfies the policy before and after, so no policy will stop it. Privileged columns need a column-level GRANT or a trigger, not a better policy.
Step 5 — which storage buckets are public?
select id, name, public from storage.buckets;Anything holding user documents should read false. Serve those through signed URLs that expire instead.
Step 6 — what can reach the database without a user?
List your deployed Edge Functions and check which ones have JWT verification turned off. Disabling it is the standard fix for making a webhook work, and it leaves a public URL — usually holding a service key — that answers anyone who finds it. If verification is off, replace it with a shared secret header or a signature check from the caller. Then confirm no coding agent is configured against your production project.
Which tool gets which wrong
The five failures are universal; the emphasis is not. Each of these has its own guide, because the specific default that bites you differs by platform.
| Tool | The failure it is known for |
|---|---|
| Lovable | Ships admin surfaces with no database-level authorisation — the pattern behind CVE-2025-48757, plus public buckets and unverified webhooks. |
| Bolt.new | Scaffolds Vite, so every VITE_ variable is compiled into the bundle. Keys leak before anything else does. |
| Replit | Agents with production credentials and the authority to run destructive migrations unprompted. |
| Claude Code & Cursor | Reads your database and your users' text in one session, which turns prompt injection into a data-exfiltration path. |
What to watch after the audit
- Row Level Security switched off on a table — one statement, no visible effect, the table becomes public.
- The service key used from a browser — the observable symptom of a secret in the bundle, and the highest-confidence signal in Supabase logs.
- A new policy that evaluates to true — usually added to unblock a feature, never narrowed afterwards.
- Function calls with no authenticated user — especially any function that touches the database or spends money.
- Signup and request bursts — bot registration and drained API keys both show up here well before the invoice does.
Where Defencecore fits
The audit is yours to run, and it is worth an hour of anyone's time. What it cannot do is stay true: on a project still being edited by an AI, the configuration you just verified has a shelf life measured in prompts. Defencecore reads your Supabase logs continuously, opens an incident when one of the signals above fires, and shows the evidence behind it. Read-only, so it can explain a problem and never cause one.
Frequently asked questions
- Is vibe coding secure?
- The code an AI writes is usually no worse than the code a junior developer writes. The difference is that nobody reviews it and nobody configures what sits around it. Almost every breach in an AI-built app comes from configuration — a key in the bundle, Row Level Security never enabled, a storage bucket left public — rather than from a flaw in the generated logic.
- How do I audit an app I built with AI?
- Four checks cover most of it, and all four are mechanical. Grep your production build for secret keys. Run one SQL query to list tables without Row Level Security. Run a second to list policies that evaluate to true for everyone. List your storage buckets and check which are public. The queries are on this page and take about thirty minutes end to end.
- Why does the preview look fine when the app is exposed?
- Because a preview runs as you, signed in, with your own data. A missing Row Level Security policy does not break anything you can see — it only means someone else's session would also succeed. Every failure on this page is invisible from inside the app, which is why they survive to production.
- Can I just ask the AI to make the app secure?
- It will produce plausible policies, and some will be right. What it cannot do is verify the result against your live project, because it is writing code rather than querying your database. Ask for the fixes, then run the queries on this page against production to confirm they landed — the model's confidence is not evidence.
- Is an audit enough, or do I need monitoring?
- An audit is true for the day you run it. The next prompt can disable Row Level Security, add an unauthenticated function, or swap in a service key, and the preview looks identical each time. On a project still being changed by an AI, the audit is the starting point and log monitoring is what keeps it true.