github

Securing AI-Built Supabase Apps Before Launch

Guides · Updated 3 October 2026

AI code generators are good at getting a Supabase app working. They are much worse at securing it, and the gap is usually in the database, not the code. This guide covers why that happens, the mistakes to expect, and what to check before you launch.

Why generated code misses RLS

Most generated apps talk to Supabase straight from the browser. There is no server between the user and the database, so row level security (RLS) is the only thing deciding who can read what. If RLS is missing or wrong, nothing else catches it.

Generators create tables with SQL or migrations. Supabase turns RLS on by default only for tables made in the dashboard Table Editor, so generated tables start with RLS off.

The model is also optimizing for the wrong thing. It learned from tutorials that skip security, and it is judged on whether the app runs. A policy of USING (true) makes the error go away and the feature work, so by that measure it is correct. Security on Supabase is mostly Postgres configuration, and the model sees your prompt and a few files, not who your users are or which data is sensitive.

What this has looked like

CVE-2025-48757, published in May 2025, covered apps generated by Lovable. Researchers scanned 1,645 apps from its showcase and found more than 170 with critical RLS flaws. The exposed data included names, emails, phone numbers, home addresses, payment information and API keys.

Broader studies point the same way. Veracode tested more than 100 models in 2025 and found that 45% of AI-generated code introduced an OWASP Top 10 vulnerability. A Carnegie Mellon benchmark found that 82.8% of the AI solutions that worked correctly were still insecure.

The pattern does not depend on the tool. It shows up wherever a generator writes the schema and nobody reviews it.

The usual mistakes

  • Tables with RLS off. Anyone with the anon key can read and write them.
  • Policies with USING (true), added to make a query work.
  • Policies written but RLS never enabled, so they are not enforced.
  • Policies with no TO clause, which also apply to visitors who are not signed in.
  • Admin checks that read user_metadata, which users can edit themselves.
  • Update policies on tables that also hold a role, plan or balance column. RLS is per row, so users can change those columns on their own rows.
  • SECURITY DEFINER functions in the public schema, callable through the API and able to bypass RLS.
  • Email confirmation turned off, so anyone can create a signed-in account with a fake address.

Keys in the frontend

The anon key belongs in the browser. The service role key does not. It bypasses RLS, and generated code sometimes uses it on the client because it makes permission errors disappear.

Search your project for SERVICE_ROLE next to a public prefix such as NEXT_PUBLIC_, VITE_, REACT_APP_ or EXPO_PUBLIC_, and search the built JavaScript for the key itself. Check that no .env file is committed to git. If the key has been exposed, rotate it.

The same goes for other secrets. Keys for third-party services belong on the server, not in a table the API can read.

What to check before launch

Run this in the SQL Editor. It shows every table in the public schema with its RLS status and number of policies.

SELECT
  t.tablename,
  t.rowsecurity AS rls_enabled,
  count(p.policyname) AS policy_count
FROM pg_tables t
LEFT JOIN pg_policies p
  ON p.schemaname = t.schemaname AND p.tablename = t.tablename
WHERE t.schemaname = 'public'
GROUP BY t.tablename, t.rowsecurity
ORDER BY t.rowsecurity, policy_count;
  • rls_enabled false: the table is open. Enable RLS and add policies.
  • rls_enabled false with policies: the policies are not enforced.
  • rls_enabled true with zero policies: the API returns nothing from this table, and the app is probably broken there.

Then read the policies themselves. This lists the ones that allow every row or apply to every role.

SELECT tablename, policyname, cmd, roles, qual, with_check
FROM pg_policies
WHERE schemaname = 'public'
  AND (roles = '{public}' OR qual = 'true' OR with_check = 'true')
ORDER BY tablename, policyname;

Last, test from the outside. The SQL Editor runs as postgres and bypasses RLS, so a query that works there proves nothing. Call the API with only the anon key and confirm that private tables return no rows.

Getting better output from the generator

You can ask for the right thing up front. Put rules like these in your project instructions:

  • Enable RLS in the same migration that creates a table.
  • Write one policy per operation, each with a TO clause.
  • Use (SELECT auth.uid()) = user_id for ownership, with both USING and WITH CHECK on updates.
  • Use app_metadata, not user_metadata, for roles.
  • Never use the service role key in client code.

This helps, but do not rely on it. In the Carnegie Mellon study, adding security hints to the prompt did not close the gap. Check the database, not the prompt.

Check again after every change

Every new prompt can add a table or loosen a policy, so one review at launch is not enough. Repeat the checks after each schema change. Locksoup runs these checks automatically with a read-only role.

Related checks

Check your own project

Locksoup runs these checks on your Supabase database in about a minute, with a read-only role, and gives you the SQL to fix what it finds.

Check my project