How-To
How to Fix Supabase RLS Fast: Founder-Friendly Playbook
April 22, 2026 · 10 min read · PolyDefender Research Team
The exact workflow to identify missing Row Level Security, patch policies safely, and validate fixes in production.
Supabase Row Level Security (RLS) is the database-layer guard that determines which rows each user can read, insert, update, or delete. Without it, a single exposed API key or a single IDOR vulnerability gives an attacker unrestricted access to every row in your database. In PolyDefender's closed-beta scans, missing or misconfigured RLS was the most frequently repeated critical finding across Lovable, Bolt.new, and Replit applications.
What RLS Actually Protects Against
RLS is a PostgreSQL feature that Supabase enforces at the query level. When a user makes a request through the Supabase client, the database checks the active policy before returning any rows. With RLS enabled and a correct ownership policy, even if an attacker finds a way to query your database directly—through an exposed anon key, a misconfigured API endpoint, or a leaked service URL—they only get rows they own.
Without RLS, the database behaves as if every query comes from a database superuser. Every authenticated user can read every row in every table your app exposes. In apps with a shared Supabase project across multiple user accounts, this means complete cross-tenant data exposure.
Step 1: Identify Every Table That Needs RLS
Start by inventorying. In the Supabase dashboard, go to Table Editor and look for the shield icon next to each table name. A red or open shield means RLS is disabled. Alternatively, run this query in the SQL Editor:
SELECT tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public' ORDER BY rowsecurity ASC;
Any table with rowsecurity = false that contains user data, financial records, messages, files, or sensitive configuration is a critical finding. Prioritize tables that your frontend reads directly through the Supabase client.
Step 2: Enable RLS and Write Ownership Policies
Enabling RLS without adding a policy creates a default-deny state: nobody can read or write anything. That is the correct starting point. Then add policies that explicitly allow the right access.
- ▸ALTER TABLE your_table ENABLE ROW LEVEL SECURITY; — enables RLS, default-deny
- ▸CREATE POLICY "users see own rows" ON your_table FOR SELECT USING (auth.uid() = user_id); — read access for row owner
- ▸CREATE POLICY "users insert own rows" ON your_table FOR INSERT WITH CHECK (auth.uid() = user_id); — write access for row owner
- ▸CREATE POLICY "users update own rows" ON your_table FOR UPDATE USING (auth.uid() = user_id); — update access for row owner
- ▸CREATE POLICY "users delete own rows" ON your_table FOR DELETE USING (auth.uid() = user_id); — delete access for row owner
The critical principle: always use auth.uid() from Supabase's built-in JWT parsing, never a user_id passed as a query parameter from the client. Client-supplied IDs can be tampered with. auth.uid() cannot.
Step 3: Handle the service_role Key
The service_role key bypasses RLS entirely. It is intended for server-side admin operations only—database migrations, cron jobs, admin dashboards running behind authentication. If you find service_role in any client-side code, that is an immediate critical incident regardless of your RLS state.
- ▸Search your entire codebase for service_role
- ▸If found in frontend code, rotate the key immediately in Supabase → Settings → API
- ▸Replace with the anon key for client-side operations; RLS policies will handle the access control
- ▸For admin operations, move them to a server-side API route or Edge Function that runs with service_role but is protected by your own authentication middleware
Step 4: Write Tests That Prove Your Policies Work
Policies that have not been tested are policies that may not work as intended. For every table with RLS, write two tests: a positive test confirming the owner can access their own rows, and a negative test confirming a different user is blocked.
In a Node.js test suite, you can use the Supabase client with a service_role key to seed test data under user A, then switch to a client authenticated as user B and confirm that SELECT returns zero rows for user A's data. These tests should run in CI so that future changes—including AI-generated code changes—cannot silently weaken your policies.
Step 5: Validate in Production After Deploying
After deploying policy changes, run a PolyDefender scan on your production URL. The scanner tests anonymous and authenticated access paths against your live database policies, flags service_role misuse, and confirms RLS coverage across all tables it can detect. A clean scan result is your evidence that the fix is working end-to-end, not just in your local test environment.
Common RLS Mistakes That Get Missed
- ▸Writing a SELECT policy but forgetting INSERT, UPDATE, and DELETE — attackers can still write data even if they cannot read it
- ▸Policies that use (true) as the USING clause — this means every authenticated user can access every row, which is usually a mistake
- ▸Using a joined column from another table as the ownership check without a subquery — policy evaluations can be bypassed if the join conditions are not tight
- ▸Forgetting to enable RLS on newly created tables after the initial fix — add RLS to your table-creation checklist, not just your remediation checklist
Need a fast security baseline?
Run a free scan to detect secrets, auth bypass, RLS exposure, injection paths, and dependency risk in minutes.