Supabase Row Level Security: Beginner's Guide 2026
Master RLS policies in Supabase to secure your database at the row level. Learn policies, auth setup, and avoid common pitfalls with production-ready examples.
TL;DR
Supabase Row Level Security (RLS) lets you enforce database-level access control automatically. Enable RLS on tables, create policies tied to authenticated users, and Supabase handles authorization transparently. Common mistakes: forgetting to enable RLS, writing overly permissive policies, and not testing with verified_user claims.
What is Row Level Security?
Row Level Security (RLS) is a PostgreSQL feature that filters query results based on the user executing the query. Instead of handling authorization in your application layer, RLS moves it to the database—meaning even direct database connections respect your policies.
Supabase exposes this via the Auth service. When a user authenticates, they receive a JWT token containing their user_id and custom claims. Every request to your database through Supabase clients automatically includes this token, and PostgreSQL evaluates RLS policies against it.
Why this matters: If your app has a vulnerability, attackers can't bypass RLS. It's defense in depth.
Enabling RLS: The Foundation
RLS is disabled by default in Supabase. Enable it explicitly:
```sql ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY; ```
Without this, all policies are ignored. This is the first mistake developers make—they write perfect policies and nothing happens because RLS isn't enabled.
Verify in official docs for your Supabase SDK version (we're targeting v2.x stable in 2026).
Authentication Context
Supabase RLS policies reference auth.uid(), which returns the current user's UUID from the JWT:
```sql -- This function returns the logged-in user's UUID SELECT auth.uid(); ```
For policies to work, users must authenticate. Anonymous requests have auth.uid() = NULL. You can explicitly allow or deny anonymous access per policy.
Writing Your First Policy
Here's a realistic scenario: users should only see their own posts.
```sql -- Enable RLS first ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;
-- Policy: Users can SELECT only their own posts CREATE POLICY "Users can view own posts" ON public.posts FOR SELECT USING (auth.uid() = user_id);
-- Policy: Users can INSERT posts (owned by them) CREATE POLICY "Users can insert own posts" ON public.posts FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Policy: Users can UPDATE their own posts CREATE POLICY "Users can update own posts" ON public.posts FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
-- Policy: Users can DELETE their own posts CREATE POLICY "Users can delete own posts" ON public.posts FOR DELETE USING (auth.uid() = user_id); ```
Key differences:
USING: Filters which rows the user can see/modify (applies to SELECT, UPDATE, DELETE)WITH CHECK: Validates data being written (applies to INSERT, UPDATE)Common Error Messages (And How to Fix Them)
Error 1: Permission denied for schema "public"
``` Error: permission denied for schema "public" Code: 42501 ```
Cause: Your Supabase service role used to query the table, but no policy allows it. Service roles bypass RLS—if you're seeing this, check your SDK configuration.
Fix: Ensure you're using the anon key (client-side) not the service_role key, or set X-Client-Info headers correctly.
Error 2: "new row violates row level security policy"
``` Error: new row violates row level security policy "Users can insert own posts" Code: 42601 ```
Cause: Your INSERT or UPDATE tries to set a user_id that doesn't match auth.uid().
Fix: Validate on the client before sending. Or set user_id server-side:
```typescript const { data, error } = await supabase .from('posts') .insert([ { title: 'Hello World', content: 'My first post', user_id: user.id, // Must match auth.uid() }, ]); ```
Error 3: Rows have no policy
``` Error: query resulted in zero rows ```
Cause: RLS is enabled but no policies exist, OR the user doesn't satisfy any policy condition.
Fix: Check pg_policies table:
```sql SELECT * FROM pg_policies WHERE tablename = 'posts'; ```
If empty, write policies. If policies exist, test the USING condition manually:
```sql SET request.jwt.claims = jsonb_build_object('sub', 'your-user-uuid'); SELECT * FROM posts; -- Now test ```
Advanced: Role-Based Policies
For apps with admin/moderator roles, add a role column to your users table and reference it:
```sql CREATE TABLE public.users ( id uuid PRIMARY KEY REFERENCES auth.users(id), email text, role text DEFAULT 'user' -- 'user', 'admin', 'moderator' );
-- Admins can view all posts CREATE POLICY "Admins view all posts" ON public.posts FOR SELECT USING ( (SELECT role FROM public.users WHERE id = auth.uid()) = 'admin' OR auth.uid() = user_id ); ```
Performance note: This subquery runs per row. For high-volume apps, store roles in the JWT claim instead [using Supabase custom claims](https://supabase.com/docs/guides/auth/jwt).
Testing RLS Locally
Use the [Supabase CLI](https://supabase.com/docs/guides/cli) to run tests:
```bash supabase test db ```
Write PostgreSQL tests:
```sql BEGIN; -- Set JWT for test user SELECT tests.create_supabase_user('test-user', 'test@example.com'); -- This user should see only their posts SELECT * FROM posts WHERE user_id = 'test-user'::uuid; ROLLBACK; ```
Verify in official docs for your Supabase version when setting up local testing.
The Service Role Exception
The service_role key (backend-only) bypasses all RLS policies. Use it only for:
Never expose the service role key to the client.
Related Guides
What am I missing?
RLS is powerful but has edge cases. What patterns have tripped you up? Are there specific role-based scenarios you'd like expanded? Drop corrections or questions in the comments—especially if you've hit issues with custom claims, performance at scale, or multi-tenancy setups.
---
Official Resources: