How to Check if Your AI-Built App Exposes User Data
A 20-minute test for an AI-built app on Supabase: list tables without row level security, read your own data as a stranger would, and fix what leaks.
To check whether your AI-built app exposes user data, do two things: list every table in your database and confirm row level security is on, then request your own data using only the public key, the way a stranger would. If that request returns rows when nobody is signed in, your data is open. The whole check takes about 20 minutes and needs no security background.
This guide assumes your app uses Supabase, which is where a recent wave of exposures happened, covered in our analysis of the exposed Supabase databases reported in September 2026. The same logic applies to any backend that hands your browser a public key. Only run these tests against projects you own.
Why your own screens cannot tell you
When you click through your app signed in as yourself, you only see data you are meant to see, or data the app chooses to show. The app can look perfectly private while the database behind it answers anyone who asks. A builder that produces working screens has no reason to flag a table that is open to the public key, because nothing on the screen breaks.
So the test has to bypass your app and ask the database directly.
Step 1: List every table and its RLS status
Open the SQL editor in your project and run:
select schemaname, tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;The rowsecurity column is true when row level security is enabled for that table. Sorting by it puts the false rows first. Every table that holds user data, orders, messages, profiles, uploads, or anything you would not print on a billboard should say true.
Supabase's row level security guide explains what the setting does: once RLS is enabled, "no data is accessible through the API when using a publishable key, until you create policies." A false here is the red flag.
Some tables are meant to be public, such as a list of blog posts or product categories. Write those down as deliberate exceptions so the next audit does not rediscover them.
Step 2: Read the policies you actually have
Enabled is not the same as correct. List the policies:
select tablename, policyname, roles, cmd, qual
from pg_policies
where schemaname = 'public'
order by tablename;Scan the qual column for these patterns:
What you see | What it means |
|---|---|
| Every row is visible to the roles listed. Usually a mistake. |
| Signed-out visitors can use this policy. |
No | The policy is not tying rows to a signed-in user. |
| Writes are as open as reads. |
A healthy policy for private rows looks like the one in Supabase's docs, which limits reads to the signed-in owner:
create policy "Individuals can view their own todos."
on todos for select
to authenticated
using ( (select auth.uid()) = user_id );AI builders often generate a policy like using (true) to make an error go away. It works, and it removes the protection. If you see one, ask why it exists before deleting it.
Step 3: Check what the signed-out role can touch
Run:
select table_name, privilege_type
from information_schema.role_table_grants
where grantee = 'anon' and table_schema = 'public'
order by table_name, privilege_type;This lists what the anonymous role is granted. Supabase's guide to securing the Data API explains the layering: grants decide which roles can reach a table, and RLS policies decide which rows they can see. Tables exposed "without RLS can be accessed by any role with matching grants." Broad grants are fine only when RLS is doing the filtering.
Step 4: Ask the database as a stranger
Now test from outside. You need your project URL and your publishable (anon) key, both of which are already visible in your front-end code, which is exactly why anyone can use them. Replace the placeholders and use a table name from step 1:
curl "https://YOUR-PROJECT.supabase.co/rest/v1/YOUR_TABLE?select=*&limit=5" \
-H "apikey: YOUR_PUBLISHABLE_KEY" \
-H "Authorization: Bearer YOUR_PUBLISHABLE_KEY"Read the result:
An empty list, `[]`: the table is protected from the signed-out role, or it is empty. Check a table you know has rows.
Rows come back: anyone who knows your project URL can read this table. Treat it as an incident.
An error about permissions: the role has no grant on that table, which is also safe.
Repeat for every table that holds user data. This is the single most useful test in the guide, because it matches what an attacker can do.
Step 5: Check the key in your code
The service_role key bypasses RLS entirely, and Supabase's docs say to keep it server-side. Search your front-end code and your repository history for it. If it appears in browser code, or was ever committed to a public repo, rotate it now. An AI assistant that "fixed" a permissions error by swapping in the service key is a classic way this happens, and coding agents can leak keys in other ways too. Secrets belong in server-side configuration, as set out in environment variables and secrets in an AI-built app.
Fixing what you find
Enable RLS on the table:
alter table public.your_table enable row level security;Write the narrowest policies that make the app work, usually "owner can read and change their own rows".
Re-run the stranger test from step 4 and confirm it now returns nothing.
Re-test the app signed in as an ordinary user, and as a second user, to confirm each sees only their own data.
Enabling RLS with no policies will lock the app out of that table, which is the safe failure. Add policies until the app works, rather than switching RLS off. If the test showed real exposure, work through the response steps for a data breach: find out what was readable, rotate credentials, and decide who must be told.
Make it a habit
Put the three SQL queries and the stranger test into your pre-launch routine, next to the checks in how to test an AI-built app before launch. Run them again whenever an AI assistant adds a table or changes a migration, since new tables are where gaps appear. For a deeper review of generated code, see how to review AI-generated code before you ship it.
FAQ
How do I know if my Supabase database is public?
Request one of your tables with only the publishable key, as in step 4. If rows come back with nobody signed in, the table is readable by anyone who has your project URL.
Is the Supabase anon key safe to put in front-end code?
It is designed to be public, which is why row level security matters. Safety depends on RLS and policies, not on hiding the key. The service role key is different and must stay on the server.
What does enabling RLS with no policies do?
According to Supabase's documentation, no data is accessible through the API using a publishable key until you create policies. Your app will lose access to that table until you add them.
Can AI audit my RLS policies for me?
It can help you read and explain them, and it is worth asking. Verify its conclusions with the stranger test, because the test shows actual behavior rather than a description of it.
How did this land?
About the author

Developer Advocate
Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.


