security·6 min read

What Row-Level Security Actually Is — For People Who Vibe-Coded Their Backend

A plain-English explanation of Row-Level Security — the single database setting behind two of the largest AI-code security incidents of 2025.

Richard — ZipLoom

July 19, 2026

Two of the largest AI-code security incidents of 2025 — the Lovable CVE that exposed 170+ databases and the Moltbook breach that leaked 1.5 million authentication tokens — had the same root cause. Row-Level Security (RLS) was not enabled. This post explains what RLS is, why AI coding tools miss it, and how to fix it in under 10 minutes.

The problem in one sentence

Without Row-Level Security, every authenticated user of your application can read every row in every database table. The app you built with Cursor or Claude or v0 is probably in this state right now.

What Supabase actually does by default

When you create a table in Supabase, it lives in a Postgres database. Supabase gives you a client library — supabase-js, the Python client, etc. — that your application uses to read and write data. The client authenticates using either your project's `anon` key (for unauthenticated requests) or your `service_role` key (for server-side admin operations).

By default, both keys can access every row in every table. There is no automatic per-user filtering. The AI that wrote your backend generated queries like `supabase.from('users').select('*')` — and that query, by default, returns every row in the users table to whoever calls it.

This is not a Supabase bug

The default behavior is documented. Supabase explicitly recommends enabling RLS as a required step when building user-facing applications. The documentation is clear. AI coding tools simply do not read your Supabase project settings when generating code, so they cannot enable this for you.

What Row-Level Security does

RLS lets you define policies that run on every database query. A policy is a SQL expression that returns true or false for each row. If the expression returns false for a row, that row is not returned to the caller.

The most common policy: a user can only see their own rows.

sql
-- Step 1: Enable RLS on the table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Step 2: Create a policy
-- This says: for SELECT queries, only return rows where
-- the user_id column matches the currently logged-in user
CREATE POLICY "Users can only read their own posts"
ON posts
FOR SELECT
USING (auth.uid() = user_id);

-- auth.uid() is a Supabase function that returns the
-- UUID of the currently authenticated user.
-- It returns NULL for unauthenticated requests.

After these two statements, `supabase.from('posts').select('*')` still works for your application — but it now automatically filters to only return the current user's rows. You did not change the query. You changed the database.

The three RLS commands you need

sql
-- 1. Check which tables have RLS enabled
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';

-- 2. Check what policies exist on a table
SELECT * FROM pg_policies WHERE tablename = 'your_table';

-- 3. Enable RLS and add the basic user-owns-row policy
-- Run this for every table that stores user data

ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can read own rows" ON your_table
FOR SELECT USING (auth.uid() = user_id);

CREATE POLICY "Users can insert own rows" ON your_table
FOR INSERT WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Users can update own rows" ON your_table
FOR UPDATE USING (auth.uid() = user_id);

CREATE POLICY "Users can delete own rows" ON your_table
FOR DELETE USING (auth.uid() = user_id);

Important: enabling RLS without policies blocks all access

If you run `ALTER TABLE posts ENABLE ROW LEVEL SECURITY` without adding any policies, Postgres denies all access to the table by default — for everyone, including your application. Add at least one policy before testing. The service_role key bypasses RLS by design, so server-side admin operations still work.

How to check if your app is exposed right now

  1. 1Open your Supabase project dashboard.
  2. 2Go to Table Editor → select any table that stores user data.
  3. 3Look for the 'RLS disabled' indicator at the top of the table view.
  4. 4Alternatively: go to Authentication → Policies. Any table with 'RLS Disabled' in this list can be read by any authenticated user.
  5. 5If you're using the Supabase SQL editor: run `SELECT tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public';` — look for rows where rowsecurity is false.

Why AI coding tools miss this

AI coding tools read and write code files. They do not have access to your Supabase project settings, your database configuration, or your infrastructure state. When they generate a Supabase schema, they write a SQL migration or a Drizzle/Prisma schema definition. Neither format has a way to express 'also enable RLS on this table and add these policies.' The tools are not negligent — they are operating in a different layer than the one where RLS lives.

This is the structural gap that caused the Lovable incident and the Moltbook breach. Both platforms generated working application code. Both left the database configuration layer unchecked.

ZipLoom

Security scan on every deploy. Flat price. No meter.

ZipLoom checks RLS, secrets, CVEs, and license compliance before your app goes live — automatically, on every deploy. First Thread founding price: $99/yr, locked for life.

Share this article

https://ziploom.dev/blog/what-is-row-level-security-for-vibe-coders

row-level securitySupabaseRLSvibe codingdatabase securityPostgres

Related articles