Supabase Row Level Security Not Enabled: Security Guide
Supabase tables have row level security not enabled by default. Learn how to check if you're exposed, what the risk is, and how to fix it in 5 minutes.
- Supabase tables have row level security disabled by default, leaving them fully readable and writable to anyone with your public anon key.
- AI code generators (Lovable, Bolt, Cursor) skip the RLS setup step during schema creation, so most apps built with these tools are unprotected.
- You can check if your tables are exposed in 30 seconds with a simple SQL query, and enable RLS in five minutes with ALTER TABLE and CREATE POLICY.
- RLS misconfiguration is worse than no RLS because it silently looks correct while your data is still accessible.
Supabase tables have row level security not enabled by default. With it disabled, anyone with your public anon key--which lives in your frontend code--can read and write every row in the database. This is the single most dangerous default in Supabase. If you built your app with Lovable, Bolt, Cursor, or even the Supabase dashboard without explicitly enabling RLS, your production data is exposed right now.
What Is RLS, and Why Does Supabase Need It?
Row-level security (RLS) is a database feature that enforces access control at the row level, not just the table level. It says: "User A sees only rows where user_id = A's ID." Supabase uses this as its primary security mechanism because the platform's entire design assumes a public API key in your frontend.
Supabase gives you two keys: the anon key (public, for unauthenticated API calls) and the service role key (secret, for server code). The anon key is intentionally exposed because your browser needs it to make requests. RLS is what stops the anon key from reading the entire users table while browsing the login page.
Without RLS, the anon key grants full read and write access to every table. This is not a bug. This is a design choice that requires you to enable RLS on every table that has sensitive data.
Let me compare:
- RLS Enabled: User sees only rows where
auth.uid() = user_id. Attacker with anon key gets no data. - RLS Disabled: User sees all rows. Attacker with anon key reads, updates, or deletes everything.
The difference is not subtle. It's the difference between a working SaaS and a data breach.
Why AI Generators Leave RLS Off (and Why You Might Too)
Lovable, Bolt, and Cursor are designed to move fast. They generate SQL schema migrations based on your prompts. The problem: creating tables is step one. Configuring security is step two, and these tools skip it.
When you say "build me an app with a users table and a messages table," the AI generates:
CREATE TABLE users (
id uuid PRIMARY KEY,
name text
);
It doesn't generate:
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
CREATE POLICY users_can_read_own_data ON users FOR SELECT
USING (auth.uid() = id);
Why? Because security requires knowledge of your business logic. It requires knowing which user ID column ties to the auth system. The AI tool cannot guess. So it defaults to "leave it to the developer."
The developer, especially during prototyping, forgets. The app works locally because the local development database is either empty or you're testing as an authenticated user. The app ships to production. Two weeks later, you wake up to a support email: someone found your anon key in your GitHub actions logs and exported your entire users table.
This has happened. The Vibe App Scanner found this vulnerability in multiple production deployments, all created with AI generators.
This is not stupidity. It's a gap between the speed of schema generation and the discipline of security hardening. If you used an AI generator, you almost certainly have this problem.
Check If Your Supabase Tables Are Unprotected (SQL Audit)
The quickest way to know: run this query in your Supabase SQL editor (your own project, authenticated as a superuser).
SELECT
table_name,
row_level_security_enabled
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name;
This returns every table in your public schema with a boolean flag. false means RLS is off. true means it's on.
Most Supabase apps will show false on most tables. This is normal and also dangerous.
If you see false on tables that contain user data--users, messages, profile, settings--those are your immediate risk. The anon key can read them.
Also check this: if a table shows true but has no policies, it's locked down for everyone--including authenticated users. You enabled RLS but forgot to add access rules. This breaks your app. We'll fix it in the next section.
To see which tables have RLS but no policies:
SELECT
t.table_name,
p.policyname
FROM information_schema.tables t
LEFT JOIN pg_policies p ON p.tablename = t.table_name
WHERE t.table_schema = 'public'
AND t.row_level_security_enabled = true
AND p.policyname IS NULL
ORDER BY t.table_name;
If this returns rows, those tables are locked. Add policies to them today.
What Happens When RLS Is Off: Real Attack Path
Let's be concrete. You're a founder who built a SaaS on Supabase. You have a users table and a subscriptions table. RLS is off on both.
Step 1: Attacker extracts your anon key. This is trivial. They open the browser's Network tab, watch your frontend make an API call, and copy the token from the request header. Or they read your bundled JavaScript--the anon key is always there, and no amount of obfuscation hides it. It's public.
Step 2: Attacker uses curl or Postman to call your Supabase REST API with that key. Replace the <PROJECT_ID> with their target project ID:
curl "https://<PROJECT_ID>.supabase.co/rest/v1/users" \
-H "Authorization: Bearer YOUR_ANON_KEY" \
-H "Content-Type: application/json"
Step 3: They get the entire users table as JSON:
[
{"id":"user-1","name":"Alice","email":"alice@example.com","password_hash":"..."},
{"id":"user-2","name":"Bob","email":"bob@example.com","password_hash":"..."}
]
Step 4: They enumerate your subscriptions table to find who paid and who didn't, or they update rows to delete other users' data, or they insert spam.
Step 5: You don't notice for weeks because there are no logs, no alerts, and no rate limiting on the REST API by default.
This is not theoretical. A real developer posted on Reddit: "Row Level Security almost broke my SaaS API." They discovered this in production after their app had been live for months. They had 50,000 users. RLS was off. An attacker could have exported the entire database.
RLS being off is equivalent to leaving your database admin password in a public GitHub repo. The only difference is that the anon key looks like a legitimate API token, so nobody questions it.
How to Enable RLS (Five Minutes)
Step 1: Go to your Supabase dashboard. Click SQL Editor. Run this for each table with sensitive data:
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
Replace table_name with each table: users, messages, subscriptions, etc. Do not enable RLS on reference tables or lookup tables (like a list of countries). Do enable it on anything user-specific or transactional.
Step 2: After enabling RLS, every table is now locked down. Authenticated users also cannot access it until you create policies. This is intentional. It forces you to think about access control.
Step 3: Test your app. Log in as a user. Try to fetch your own data. If you get an error, it means you enabled RLS but have no policies yet. That's the next section.
Why five minutes? Because ALTER TABLE takes milliseconds. The time is in the thinking: which tables need RLS?
General rule: if a table contains any data specific to a user (messages, settings, subscriptions), enable RLS. If it's just a lookup table (list of products, list of countries, list of industries), you can skip it (but enabling it does not hurt).
The one common mistake: enabling RLS without policies. RLS on, no policies equals nobody can access anything. Your app breaks. The fix is in the next section.
Write Your First Policy (Starter Pattern)
Policies are the rules that say who can access which rows. The simplest and most common policy is: "Users can read and modify only their own rows."
Here is the boilerplate for a users table:
CREATE POLICY users_can_read_own_data ON users FOR SELECT
USING (auth.uid() = id);
CREATE POLICY users_can_update_own_data ON users FOR UPDATE
USING (auth.uid() = id);
CREATE POLICY users_can_insert ON users FOR INSERT
WITH CHECK (auth.uid() = id);
CREATE POLICY users_can_delete_own_data ON users FOR DELETE
USING (auth.uid() = id);
What this does:
- SELECT: User can read only rows where their
auth.uid()matches theidcolumn. - UPDATE: User can update only their own row.
- INSERT: User can only insert a row if they set the
idto their ownauth.uid(). - DELETE: User can delete only their own row.
If a user tries to query SELECT * FROM users, they get only their own row. If they try to update someone else's row, Supabase returns a 403 Forbidden.
For a messages table, it's similar:
CREATE POLICY messages_can_read_own ON messages FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY messages_can_insert ON messages FOR INSERT
WITH CHECK (auth.uid() = user_id);
Assume the messages table has a user_id column. Now messages are readable only by the user who created them (or by system users, if you add an admin role later).
Testing: Log in as user A. Query messages. You see only user A's messages. Log in as user B. Same data, different result.
This is row-level security. Everything else is variation on this theme.
Before You Ship: Pre-Launch Checklist
Before you push to production, run this checklist:
-
Every table has RLS enabled: Run the query from the "Check If Your Supabase Tables Are Unprotected" section. All tables with user data should show
true. -
Every table has at least one policy: No table should have RLS enabled with zero policies. Query the second SQL block from that section. The result should be empty.
-
Test as unauthenticated: Open your browser's Network tab. Make a request to the Supabase REST API without a token, or with a fake token. You should get a 401 Unauthorized or an empty result. If you get data, RLS is not working.
-
Test as authenticated user A: Log in. Fetch your data. Verify you see only your rows. Try to query other users' rows by ID. You should get nothing.
-
Test as authenticated user B (different account): Repeat step 4. User B should see only user B's rows.
-
Check git history for exposed keys: Your anon key is safe to have in the repo. Your service role key should never be there. Run
git log -S "your-service-role-key"to check for accidental commits. If you find any, rotate the key immediately. -
Enable Supabase audit logs: Go to your dashboard, Auth settings. Enable detailed logging. This will not prevent attacks, but you'll have a record of what was accessed.
That's it. Seven checks. None takes more than five minutes.
If any of these fail, do not deploy. RLS misconfiguration is worse than no RLS because it looks correct but isn't.
Frequently Asked Questions
Should I enable row level security?
Yes, on every table with user-specific data. RLS is not optional if your app has multiple users. It is the primary access-control layer in Supabase. If you skip it, you have no multi-tenant isolation.
How do I enable row level security in Supabase?
Run ALTER TABLE table_name ENABLE ROW LEVEL SECURITY; in the SQL editor, then create policies with CREATE POLICY. Policies define which rows users can access. Without policies, the table is locked even to authenticated users.
Should you use row level security?
Yes, always, on any multi-user app. Even on single-tenant apps deployed with an external database, RLS is good practice. It catches bugs in your application logic. If your backend accidentally queries the wrong user's data, RLS will block it.
Is the Supabase publishable key the same as the Anon key?
Yes. Supabase documentation uses both terms interchangeably. The anon key is the public key you put in your frontend. Supabase sometimes calls it the "publishable key." Both are the same thing and both are intentionally exposed.
What is the difference between an anon key and a service role key?
The anon key is public and bypasses no security--it respects RLS policies. The service role key is secret and bypasses RLS--use it only in backend code. Never expose the service role key in the frontend or in a public repo.
Supabase row level security not enabled: Is it dangerous?
Yes. If RLS is not enabled on a table, anyone with your anon key can read, update, or delete every row, regardless of who owns it. This is equivalent to making your entire database public. Enable RLS on every table with user-specific data.
What are the disadvantages of Supabase?
Supabase is opinionated. It assumes you will enable RLS and manage policies. If you treat it like a traditional database and forget security, it is very insecure. The platform does not have built-in backups (you must configure them). Postgres is powerful but not as easy to scale horizontally as some cloud databases. For most teams, these tradeoffs are worth it. For teams that cannot write SQL, Supabase is hard.
The Bottom Line
RLS is not optional in Supabase. Without it, your anon key grants full access to your database. AI code generators will not enable it for you. You must do this before shipping.
The fix takes five minutes: enable RLS on every sensitive table, then add one policy per operation (SELECT, INSERT, UPDATE, DELETE). Test in development and in production.
If you built on Supabase and have not done this, do it today. Check your production database right now. You're probably exposed.
If you want someone else to handle this--RLS policies, backups, scaling, upgrades--Ship provides managed Postgres with Supabase integration, so you keep the flexibility without the ops burden.