new: drive vas from your AI agent over MCP · Cursor, Claude Code, Windsurf
Supabase Security

Supabase Row Level Security: setup, policies, and testing

Enable RLS, tighten Postgres grants, add ownership policies, and test anonymous plus cross-user access. The examples below cover the mistakes that expose real Supabase data.

Supabase secures its Data API with two layers: grants control which operations a role may perform, and RLS controls which rows the operation may reach. You need both. Updated September 2026.

For an implementation checklist and copyable policy patterns, see the same team's practical Supabase RLS guide.

Dashboard tables and SQL-created tables behave differently

Tables created through the Supabase Dashboard have RLS enabled by default. Tables created through SQL, migrations, or external tooling may not. For every table in an exposed schema, enable RLS in the migration, remove unnecessary grants, and test the anon and authenticated roles. A public publishable or legacy anon key is expected; unsafe permissions behind it are the vulnerability.

Read Supabase's official Data API security guidance →

Not sure if your RLS is configured correctly?

Our scanner tests your live project from the outside, the same way an attacker would. Paste your live app URL and we'll extract the Supabase config from your frontend.

RLS Policy Testing

Tests every table for read/write access

Storage Buckets

Checks upload/delete permissions

RPC Functions

Finds unprotected server functions

CRUD Permissions

INSERT, UPDATE, DELETE checks

Edge Functions

Tests unauthenticated access

Auth Config

CAPTCHA and signup settings

Scan Your App for RLS Issues (Free)

Results in 2-3 minutes; your first scan is free. Point it at your live app URL. vas extracts your Supabase config from your frontend automatically.

Quick Start: Enable RLS in 2 Minutes

1Enable RLS on Your Table

-- Enable RLS on the table
ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;

2Create a Policy for Authenticated Users

-- Users can only see their own data
CREATE POLICY "Users can view own data"
ON your_table
FOR SELECT
TO authenticated
USING ((select auth.uid()) = user_id);

-- Users can only insert their own data
CREATE POLICY "Users can insert own data"
ON your_table
FOR INSERT
TO authenticated
WITH CHECK ((select auth.uid()) = user_id);

-- Users can only update their own data
CREATE POLICY "Users can update own data"
ON your_table
FOR UPDATE
TO authenticated
USING ((select auth.uid()) = user_id);

-- Users can only delete their own data
CREATE POLICY "Users can delete own data"
ON your_table
FOR DELETE
TO authenticated
USING ((select auth.uid()) = user_id);

Important: Use (select auth.uid()) instead of auth.uid() for better performance. The select wrapper prevents re-evaluation on every row.

3Verify It Works

-- In a local or test environment, simulate a signed-in user
set session role authenticated;
set request.jwt.claims to '{"role":"authenticated","sub":"USER_UUID"}';

-- Assert this user can read their row, but not another user's row
select * from public.your_table;

-- Put repeatable allow and deny assertions in supabase/tests/
-- Then run: supabase test db

Manual testing catches simple cases, but misses edge cases like write access, storage buckets, and RPC functions. For a thorough check, run a scan against your live app.

Common RLS Patterns

User-Owned Data

Most common pattern - each user can only access their own rows:

CREATE POLICY "Users own their data"
ON posts
FOR ALL
TO authenticated
USING ((select auth.uid()) = author_id);

Public Read, Private Write

Anyone can read, but only owners can modify:

-- Anyone can read
CREATE POLICY "Public read access"
ON posts
FOR SELECT
TO anon, authenticated
USING (true);

-- Only owners can write
CREATE POLICY "Owners can modify"
ON posts
FOR UPDATE
TO authenticated
USING ((select auth.uid()) = author_id);

Team-Based Access

Users can access data belonging to their team:

CREATE POLICY "Team members can access"
ON projects
FOR ALL
TO authenticated
USING (
  team_id IN (
    SELECT team_id FROM team_members
    WHERE user_id = (select auth.uid())
  )
);

Common RLS Mistakes

Forgetting to enable RLS

Creating policies without enabling RLS does nothing. Always run:

ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;

Using auth.uid() without select wrapper

This causes performance issues - the function re-evaluates for every row:

-- Bad: USING (auth.uid() = user_id)
-- Good: USING ((select auth.uid()) = user_id)

Creating service_role policies

Service role bypasses RLS automatically. Adding a policy for it creates warnings:

-- Bad: Creates multiple_permissive_policies warning
-- Good: Just use TO authenticated (service_role bypasses RLS)

Only testing SELECT policies

Most developers test if users can read other users' data but forget to test INSERT, UPDATE, and DELETE. An attacker who can't read your data can still modify or delete it if write policies are missing.

Our scanner checks for all of these mistakes automatically.

Check Your App for RLS Mistakes

RLS Security Checklist

RLS is enabled on ALL tables containing user data
Every table with RLS has at least one policy
Policies use (select auth.uid()) not auth.uid()
No service_role key in frontend code
Policies specify TO authenticated or TO anon
Tested as anonymous user: cannot read data
Tested as anonymous user: cannot write/delete data
Storage buckets have proper access policies
RPC functions require authentication
INSERT policies use WITH CHECK, not USING

Want to check all of these automatically?

Run your first scan free

Frequently Asked Questions

Why use (select auth.uid()) instead of auth.uid() in Supabase RLS?

Wrapping auth.uid() in a SELECT subquery lets Postgres create an initPlan and cache a value that does not change per row. Supabase's performance guidance recommends this pattern for JWT and other stable functions. Also index columns used by policies and measure the real query plan before and after.

What does “RLS disabled in public” mean in the Supabase dashboard?

It means a table in an exposed schema can be reached through the Data API without row policies. Actual access also depends on Postgres grants. Enable RLS with ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;, revoke operations the role does not need, and add explicit policies for everything that remains allowed.

Is RLS enabled by default in Supabase?

Tables created through the Supabase Dashboard have RLS enabled by default. Tables created through SQL, migrations, or external tools may not. Include the RLS statement in the same migration that creates each exposed table so the protection is reproducible.

How do I enable RLS on tables created with SQL or migrations?

Add ALTER TABLE public.your_table ENABLE ROW LEVEL SECURITY; to the migration, set only the grants each role needs, and create policies for each permitted operation. Revoking default privileges can make new exposure opt-in, but it does not enable RLS by itself.

What does “Enable automatic RLS” do in Supabase?

It creates an event trigger that enables Row Level Security on new tables in the public schema. That is a useful safety net, but it does not create policies or configure least-privilege grants. Keep an explicit ALTER TABLE ... ENABLE ROW LEVEL SECURITY statement in every migration too, so the security state is visible and reproducible.

How do I disable RLS in Supabase (and when should I)?

ALTER TABLE your_table DISABLE ROW LEVEL SECURITY; turns it off for a table. The only legitimate case is read-only public reference data (country lists, public product catalogs), and even then a FOR SELECT TO anon, authenticated USING (true) policy is the safer move. Never disable RLS to fix a permission error; that error is RLS doing its job.

Do grants and RLS policies do the same thing?

No. Grants decide whether a Postgres role can perform an operation on a table, view, or function. RLS policies then decide which rows that operation can affect. PostgreSQL evaluates grants first, so a secure review must cover both layers.

How can I test if my Supabase RLS is actually secure?

Add database tests for allowed and denied SELECT, INSERT, UPDATE, and DELETE operations under both anon and authenticated. Use two authenticated users to test cross-user and cross-tenant isolation, then run supabase test db. Vibe App Scanner can also probe the deployed app from an external attacker's perspective.

Stop Guessing. Scan Your Supabase Project.

Paste your live app URL. We'll extract your Supabase config from your frontend and report RLS policies, storage buckets, RPC functions, and more in 2-3 minutes.

Run your first scan free

Your first scan is free. No card required.

Last updated: September 4, 2026