Configure Row Level Security in Supabase so users see only their own rows
Step‑by‑step guide to enable RLS on a Supabase Postgres table, create a user‑owned SELECT policy, test the behavior, and roll back if needed.
01 Sept 2026, 13:14 UTC

Desired Outcome
After completing this guide, the table public.profiles (or any table you choose) will have Row Level Security enabled and a policy that returns rows only when the user_id column matches the ID of the currently authenticated Supabase user. Unauthenticated requests or requests from other users will return an empty set.
Prerequisites
- A Supabase project with a Postgres database.
- The Supabase CLI installed and logged in (
supabase login). - A table (e.g.,
public.profiles) that includes auser_idcolumn of typeuuidreferencingauth.users(id). - Supabase JavaScript client (@supabase/supabase-js) available in your test environment.
Focused Procedure
-
Enable RLS on the table. Open the Supabase SQL editor (or create a migration file) and run:
Where to run: Supabase Dashboard → SQL editor, or via CLI withALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;supabase db pushafter adding the statement to a migration.
Permissions: Requires database admin role (provided by the Supabase project owner).
Risk: If no policy exists after enabling, all queries return zero rows. -
Create a SELECT policy that limits rows to the current user. Execute:
Explanation:CREATE POLICY user_owned_select ON public.profiles FOR SELECT USING (auth.uid() = user_id);auth.uid()extracts the user ID from the session JWT; the policy compares it to theuser_idcolumn. -
Commit the migration (if using CLI). Save the SQL statements in a migration file under
supabase/migrations/and run:
This applies the changes to the remote database.supabase db push -
Test with an authenticated user. In a Node.js or browser environment:
Expected check: The returned array contains rows whoseconst { createClient } = require('@supabase/supabase-js'); const supabase = createClient('https://.supabase.co', ''); // Sign in a test user const { data: { session }, error } = await supabase.auth.signInWithPassword({ email: '[contact removed]', password: 'secure-password', }); if (error) throw error; // Set the client to use the user's JWT supabase.auth.setSession({ access_token: session.access_token, refresh_token: session.refresh_token }); const { data: rows, error: queryError } = await supabase .from('profiles') .select('*'); console.log(rows); // Should contain only rows where user_id = session.user.iduser_idmatches the signed‑in user’sidfromsession.user. -
Verify that unauthenticated or other‑user requests return none. Using the same client without setting a session (anon client) or with a JWT from a different user:
Expected check: An empty array indicates the policy is filtering correctly.const anonSupabase = createClient('https://.supabase.co', ''); const { data: anonRows } = await anonSupabase.from('profiles').select('*'); console.log(anonRows); // Should be []
Expected Checks
- After enabling RLS, a query with no policy returns zero rows (confirm RLS is active).
- Authenticated query returns only rows owned by that user.
- Anon query or query with a different user’s token returns zero rows.
Recovery Options (Rollback)
Because enabling RLS and creating a policy change the database state, you can revert them if needed:
-
Drop the policy. Run:
DROP POLICY user_owned_select ON public.profiles; -
Disable RLS on the table. Run:
Note: Disabling RLS removes all policy enforcement; ensure you have alternative access controls if your application relies on them.ALTER TABLE public.profiles DISABLE ROW LEVEL SECURITY; -
Re‑apply a migration. If you manage changes via CLI, create a new migration that contains the drop/disable statements and run
supabase db push.
Limitations and Practical Verification
The policy shown only covers SELECT. To protect INSERT, UPDATE, and DELETE you must create additional policies using the same auth.uid() = user_id condition (or with a WITH CHECK clause for inserts/updates). Always test each operation with both authenticated and anon clients to confirm the expected behavior.
Practical verification: after each step, run the corresponding SQL or client query and inspect the output. If you see unexpected rows, double‑check that the user_id column is correctly populated and that the JWT’s sub matches the auth.users(id) value.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.