# What is Supabase "RLS"? — how AI-built apps end up with all their data public, explained without code

> "ChatGPT told me to enable RLS but I don't know what that means." This explains what Row Level Security is, and why without it every user's data is readable by anyone — no code reading required. Includes how to check your own project and an instruction to paste into your AI tool.

- Published: 2026-08-06
- Author: 友田 陽大
- Tags: バイブコーディング, セキュリティ, Supabase, データベース, 個人開発
- URL: https://tomodahinata.com/en/blog/supabase-rls-explained-for-non-engineers
- Category: Running the app you built with AI
- Pillar guide: https://tomodahinata.com/en/blog/ai-app-pre-launch-guide-for-non-engineers

## Key points

- RLS is the database-side setting that decides who can see which rows. It is unrelated to whether you have a login screen — with a login screen but no RLS, all data is still readable.
- The Supabase anon key is public by design. Being readable in the browser is normal. What's abnormal is that the public key alone can fetch all your data.
- Enabling RLS is not enough. A policy of USING (true) means 'everyone can see every row' — the same outcome as having it disabled.
- Writing the read rule (USING) and forgetting the write rule (WITH CHECK) is the most frequent gap. It lets someone create rows in another person's name.
- CVE-2025-48757 is exactly this failure, rated CVSS 9.3 CRITICAL. The vendor disputed it, arguing that protecting application data is the customer's responsibility.

---

"ChatGPT told me to enable RLS. But I don't know what it means, or how to do it."

This article is for getting out of that state. You don't need to be able to read code. Once you understand the mechanism, you can direct an AI to fix it.

---

## 1. What RLS is — think in spreadsheets

**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 your app has a table called "notes":

| id | user_id | content |
| --- | --- | --- |
| 1 | A | note with a bank PIN |
| 2 | A | shopping list |
| 3 | B | notes on a job search |

With RLS configured, when A is logged in the database returns only rows 1 and 2. Row 3 is treated as if it doesn't exist.

Without RLS, **all three rows come back no matter who asks**.

The key point is that this decision happens **inside the database**. That is a different layer of defence from writing "only show A's records" in your application code.

---

## 2. Why a login screen doesn't protect you

This is the part that most contradicts intuition.

"The app can't be used without logging in, so surely someone who isn't logged in can't see the data?" — that's the natural assumption.

### What actually happens

With a service like Supabase, **queries travel from the browser directly to the database**, without passing through your application code.

```
[What you assume]
user → login screen → your app → database

[What is also possible]
attacker ──────────────────────────→ database
```

The reason it can go direct is that both pieces of information needed to connect are **public**:

- The database URL — visible in the browser developer tools
- The `anon` (anonymous) key — likewise

And this is **not abnormal**. The `anon` key is public by design. It was built to be published.

So what provides the safety? **RLS does.** The `anon` key is only "permission to talk to the database"; what you can see is decided by RLS. That is the design.

Without RLS, the public key alone fetches everything. The accurate description is not "a key leaked" but **"nothing was ever locked."**

### An analogy

- **Login screen** = the lock on the building's front door
- **RLS** = the lock on each room's safe

Lock the front door, but leave a window open and the safes unlocked, and the contents leave the building. In the Supabase model the "window" is open from the start (that is the correct design), which makes the safe lock mandatory.

---

## 3. This has actually happened — CVE-2025-48757

This isn't theoretical.

A vulnerability was registered for apps generated by the AI app builder Lovable:

> An insufficient database Row-Level Security policy allows remote unauthenticated attackers to read or write to arbitrary database tables of generated sites

Severity: **CVSS 9.3 CRITICAL** — 9.3 out of 10, the most severe band.

### The notable part is the vendor's rebuttal

The vendor filed a **dispute** against this CVE. The substance of their argument matters.

Their position was that **protecting application data is the customer's responsibility**.

That reads less like evasion than a factually correct observation. And what it means is this — **the responsibility for protecting the data in the app you built is yours.**

The expectation that "the AI tool must be handling this automatically" has been officially denied. There is no option but to check it yourself.

---

## 4. Enabling it isn't enough — the `USING (true)` trap

When you enable RLS, your app will usually **stop working**.

That is correct behaviour. RLS is designed so that **with no policy — no grant condition — everything is denied**. The moment you enable it, not a single row comes back.

And here is the accident this article most wants to warn you about.

The app stops working, you panic, and you ask the AI to "fix it, the data stopped loading." The AI produces **the shortest path back to a working state**.

```sql
-- What an AI tends to write to "make it work again"
CREATE POLICY "..." ON notes FOR SELECT USING (true);
```

`USING (true)` means **"always allow"** — that is, "everyone may see every row." The app works. And in security terms, **the outcome is identical to having RLS disabled**.

Because RLS now displays as "enabled", you feel you have verified it. This is the most dangerous pattern of all.

### The correct condition

To scope to the owner, you write:

```sql
CREATE POLICY "read only my own notes" ON notes
  FOR SELECT
  USING ( (select auth.uid()) = user_id );
```

`auth.uid()` is "the ID of whoever is asking right now." Allow only when it matches the row's `user_id` — that is what "the owner's rows only" means.

The `(select auth.uid())` form is written that way for performance. A bare `auth.uid()` is evaluated per row and becomes dramatically slow as row counts grow. It is the form recommended in Supabase's own documentation.

---

## 5. The most frequent gap — forgetting writes

Writing the read restriction and **forgetting the write restriction**. This is the gap I see most often in practice.

- `USING` → "which rows you may **see / touch**"
- `WITH CHECK` → "what values you may **create / change a row into**"

With only `USING` written, this becomes possible:

- Creating a note that specifies someone else's `user_id`
- Rewriting your own note's `user_id` to someone else's, pushing it onto them

Neither is readable, but both are writable, so normal testing never reveals it.

### The correct shape

```sql
-- Insert: you can only create rows in your own name
CREATE POLICY "insert only my own notes" ON notes
  FOR INSERT
  WITH CHECK ( (select auth.uid()) = user_id );

-- Update: you may touch only your rows, and they must stay yours
CREATE POLICY "update only my own notes" ON notes
  FOR UPDATE
  USING ( (select auth.uid()) = user_id )
  WITH CHECK ( (select auth.uid()) = user_id );
```

UPDATE needs both because "which rows you may touch" and "what you may turn them into" are separate questions. With only one, someone can rewrite their own row into someone else's.

---

## 6. How to check for yourself

You can do this without reading any code.

### Step 1: Check enabled/disabled in the dashboard

1. Log in to the Supabase dashboard
2. Open **Table Editor** in the left menu
3. Look at the table list

Any table showing a warning badge such as "RLS disabled" or "Unrestricted" is **a fully public table**.

### Step 2: Look at what the policies say

1. Open **Authentication** → **Policies**
2. Read each table's policies one at a time
3. Look for conditions (USING / WITH CHECK) that contain only `true`

A policy that is only `true` restricts nothing.

### Step 3: Count whether write policies exist

Look for tables that have a SELECT policy but no INSERT / UPDATE policy. If there is none, that operation is either denied (fine) or permitted loosely by another policy (dangerous).

---

## 7. The instruction to paste into your AI tool

If you'd rather have an AI do the checking above, paste this as-is.

```text
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)
(4) Tables that have a SELECT policy but no WITH CHECK on INSERT / UPDATE

Then write the RLS policy SQL that scopes each table to "only the logged-in
owner", observing the following:

- For performance, write auth.uid() in the form (select auth.uid())
- Always attach WITH CHECK to INSERT and UPDATE
- Attach both USING and WITH CHECK to UPDATE, and write the reason as a SQL comment
- If anywhere bypasses RLS using the service_role key, list those too

I cannot read SQL, so explain in one plain sentence per policy who is being
allowed to do what.
```

To check whether the policy you wrote actually scopes anything, paste it into the [free RLS checker](/aegis/rls-checker). It runs entirely in the browser, so your SQL is never transmitted anywhere.

---

## 8. Common misconceptions

**"If I make the table private I don't need RLS."**
The Supabase API automatically exposes tables in the public schema. Making them private is a separate setting, and the tables your app uses have to be exposed anyway. It is not a substitute for RLS.

**"Using the service_role key is easier."**
It certainly works — because it bypasses RLS entirely. But the moment that key reaches the browser, all data is readable and writable by anyone. AI-generated code reaches for this key precisely because **no error appears**. A textbook case of the easy path being the most dangerous.

**"It's only for me, so it's fine."**
If it really is only you and the URL isn't published, the risk is limited. But if you're thinking "maybe I'll show people later," there's no guarantee you'll configure it then. Doing it now is cheaper.

**"I'll think about it once I have users."**
RLS can be added later, but **data that has already leaked does not come back**. And retrofitting RLS breaks all your existing queries at once, which is exactly the situation that produces a panicked `USING (true)`.

---

## Summary

- RLS is the database-side setting that decides who can see which rows
- With Supabase, queries go from the browser straight to the database, so a login screen is not a defence. The `anon` key being public is normal; the absence of RLS is what's abnormal
- Enabling it isn't enough — `USING (true)` produces the same outcome as disabled
- You need writes (`WITH CHECK`) as well as reads (`USING`)
- On where responsibility lies, the tool vendor itself has stated it rests with the user

RLS is where AI-built apps have the most incidents. Which also means closing it removes your single largest risk. The configuration isn't hard — **it just gets forgotten**.

If you don't yet have the full picture, start from [Before you launch the app you built with AI — a complete pre-launch guide for non-engineers](/blog/ai-app-pre-launch-guide-for-non-engineers), which also covers what else to check.
