Supabase Row-Level Security: How to Check If Your Data Is Exposed

By Pouyan · Updated August 2026

Of everything I check on an app built with AI, this is the one I check first, and it is wrong more often than anything else I look at.

Not because the people building these apps are careless. Because nothing in the process ever tells you it is missing. You ask for a place to store users. You get a place to store users. Nobody mentions that you also needed to say who is allowed to read it, and the app works perfectly either way.

That is the whole problem in one sentence. A database with no rules and a database with correct rules look identical from the outside when you are the one using the app. The difference only shows up when somebody who is not you goes looking.

What is actually going on

Supabase gives your app two keys. One is meant to be public. It sits in the code your website sends to every visitor, and that is fine, that is what it is for. It is not a password.

What stops that public key reading your entire users table is a thing called Row-Level Security, and it is off by default on a table you or your agent created directly. With it off, that public key can read everything. Names, emails, whatever else you put in there. No login, no cleverness, no hacking in any meaningful sense. Someone just asks, and your database answers.

I want to be precise about how easy this is, because people assume there is a difficulty barrier protecting them. There is not. Your key is in your page source. The address of your database is in your page source. Reading the table is one request that anyone can type.

How to check, in about two minutes

Open your Supabase dashboard and go to the Table Editor. Look down the list of tables for a badge that says Unrestricted.

That word means exactly what it sounds like. That table has no rules, and anyone holding your public key can read it. If you see it on a table containing anything about a person, that is your answer and you can stop reading this section.

If every table says otherwise, you are probably fine, but understand what you have confirmed. You have confirmed that rules exist, not that the rules are correct. A policy can be switched on and still say something far more generous than you intended. That is a harder thing to eyeball, and it is the part where a second pair of eyes genuinely helps.

Why so many people leave it off

This is the bit I have sympathy for, and it explains almost every exposed database I have seen.

You turn Row-Level Security on. Your app immediately stops working. Not subtly, either. Blank screens, empty lists, your own account unable to see its own data. It looks like you broke everything, so you turn it back off, the app works again, and you promise yourself you will come back to it properly later.

Nothing was broken. Switching RLS on with no policies means nobody can read the table, including you. The rules are working exactly as designed and you have not written the ones that let the right people in yet.

So do them together, and do them one table at a time. Enable it on a single table, write the rule for that table, confirm your app still works, then move to the next. It is slower and it is the only version that actually gets finished.

The rule you almost certainly want

For any table where each row belongs to one person, the rule is the same sentence in English: you can read a row if it is your row. In practice that means matching the row's user id against the id of whoever is asking.

alter table profiles enable row level security;

create policy "people read their own profile"
  on profiles for select
  using (auth.uid() = user_id);

Get that working on one table. Then repeat the shape. Most apps need three or four variations of it and nothing more exotic, and the mistake is trying to design the whole permission system before shipping any of it.

The one that is genuinely dangerous

There is a second Supabase key, usually called the service role key, and it ignores Row-Level Security completely. That is its job. It is meant to live on a server where visitors cannot reach it.

If that key ever ended up in your frontend, none of the work above matters, because that key is allowed to bypass all of it. Open your live site, view source, and search for it. Worth ninety seconds even if you are confident. There is more on this in the guide about exposed keys.

If you want someone to check it with you

Send me your app's address. I will look at it myself before we speak, including trying the read a stranger would try, then we take half an hour and I will tell you plainly whether anything came back. It is free and there is no pitch at the end of it. If your rules are already right, that is a good outcome and I will say so.

Common questions

Is it safe for my Supabase anon key to be public?

Yes, by design. The anon key is meant to sit in your frontend. It is only safe while Row-Level Security is switched on for every table, because RLS is the thing that decides what that key is allowed to read. The danger was never the key being visible. It is RLS being off behind it.

How do I check whether my data is exposed?

Open the Supabase Table Editor and look for a table marked Unrestricted. That badge means anyone holding your public key can read it. For a second opinion from outside, try an anonymous read of the table the way a stranger would, and see whether rows come back.

Can turning on RLS break my app?

Yes, and this is why people leave it off. Switching RLS on with no policies blocks everyone, including your own logged-in users, so the app looks broken. The fix is to do them together: enable RLS on a table and write its policy in the same sitting, one table at a time.

What does a basic policy look like?

For a table where each row belongs to one person, the usual rule is that someone can read a row when the row's user id matches their own. Start there, get it working on one table, then repeat the pattern rather than trying to secure everything at once.

Not sure if yours is affected?

Send me your app's address. I'll look at it myself before we speak, then we take half an hour and I'll tell you what I found in normal words. Free, no pitch, and if it's in good shape I'll tell you that too.

Would rather just talk? Book a call without filling this in. Bring your app's address and we'll look at it together.

Free · 30 minutes · no signup · no card

Pouyan Ahmadpour · engineer · Amsterdam

Keep reading