$ cat /posts/supabase-row-level-security-rls-database-access-control.md
[tags]Supabase

Supabase Row Level Security: Database Access Control

drwxr-xr-x2026-01-255 min0 views
Supabase Row Level Security: Database Access Control

Row Level Security (RLS) is PostgreSQL's most powerful security feature, enabling database-level access control where policies determine which rows users can view, insert, update, or delete based on their authentication status and custom rules. Unlike application-level security that can be bypassed with direct database access or API manipulation, RLS enforces permissions at the database layer making it impossible to circumvent from client applications. This comprehensive guide covers understanding RLS concepts and importance, enabling RLS on tables, creating policies for SELECT, INSERT, UPDATE, and DELETE operations, using auth.uid() for user-specific data access, implementing multi-tenant applications with RLS, testing policies effectively, common patterns for blogs, social apps, and SaaS platforms, and debugging RLS issues. Mastering RLS is absolutely critical for production Supabase applications—without proper RLS policies, your data is publicly accessible regardless of authentication. Before proceeding, understand database basics, foreign keys, and authentication.

What is Row Level Security?

Row Level Security is a PostgreSQL feature that restricts which rows a user can access in database queries. Each table can have multiple policies that control SELECT (read), INSERT (create), UPDATE (modify), and DELETE (remove) operations. Policies use SQL expressions that evaluate to true or false for each row, determining access. The key function auth.uid() returns the currently authenticated user's ID, enabling user-specific policies. RLS works seamlessly with Supabase authentication, automatically applying policies based on JWT tokens.

Why RLS is Critical

Without RLSWith RLSSecurity Level
All data publicly accessibleUsers only access authorized dataDatabase-enforced
Client code controls accessPostgreSQL enforces policiesUnbypassable
Easy to bypass with API toolsImpossible to bypassProduction-ready
Must validate in applicationAutomatic validationLess code
Security bugs commonCentralized securityReduced risk

Enabling RLS on Tables

sqlenable_rls.sql
-- Enable RLS on a table
alter table posts enable row level security;

-- Without policies, table becomes inaccessible (secure by default)
-- Users can't read, insert, update, or delete ANY rows

-- Disable RLS (not recommended for production)
alter table posts disable row level security;

-- Check if RLS is enabled
select tablename, rowsecurity 
from pg_tables 
where schemaname = 'public';

-- Enable RLS on multiple tables
alter table users enable row level security;
alter table posts enable row level security;
alter table comments enable row level security;
alter table profiles enable row level security;
Critical: Once RLS is enabled without policies, the table becomes completely inaccessible to all users (including authenticated users). You must create policies to grant access. Always enable RLS on new tables immediately after creation.

Creating Basic Policies

sqlbasic_policies.sql
-- Public read access (anyone can view)
create policy "Public posts are viewable by everyone"
on posts for select
using ( true );

-- Users can only view their own posts
create policy "Users can view their own posts"
on posts for select
using ( auth.uid() = user_id );

-- Users can insert their own posts
create policy "Users can create their own posts"
on posts for insert
with check ( auth.uid() = user_id );

-- Users can update their own posts
create policy "Users can update their own posts"
on posts for update
using ( auth.uid() = user_id )
with check ( auth.uid() = user_id );

-- Users can delete their own posts
create policy "Users can delete their own posts"
on posts for delete
using ( auth.uid() = user_id );

-- Policy breakdown:
-- for select/insert/update/delete: Operation type
-- using: Check before operation (existing rows)
-- with check: Validate new/updated data

Understanding auth.uid()

sqlauth_uid.sql
-- auth.uid() returns the authenticated user's ID from JWT token
-- Returns NULL for unauthenticated users

-- Example: Users table with self-access only
create table profiles (
  id uuid references auth.users(id) primary key,
  username text unique,
  bio text,
  avatar_url text
);

alter table profiles enable row level security;

-- Users can only view their own profile
create policy "Users can view own profile"
on profiles for select
using ( auth.uid() = id );

-- Users can update their own profile
create policy "Users can update own profile"
on profiles for update
using ( auth.uid() = id );

-- Anyone can insert their profile (after signup)
create policy "Users can insert own profile"
on profiles for insert
with check ( auth.uid() = id );

-- Testing auth.uid()
-- When logged in as user with ID 'abc-123':
select auth.uid(); -- Returns 'abc-123'
-- When not logged in:
select auth.uid(); -- Returns NULL

Common RLS Patterns

sqlrls_patterns.sql
-- Pattern 1: Blog posts (public read, owner write)
alter table posts enable row level security;

create policy "Posts are publicly readable"
on posts for select using ( true );

create policy "Authors can CRUD their posts"
on posts for all
using ( auth.uid() = author_id );

-- Pattern 2: Private messages (sender/receiver only)
alter table messages enable row level security;

create policy "Users can view their messages"
on messages for select
using ( 
  auth.uid() = sender_id or 
  auth.uid() = receiver_id 
);

-- Pattern 3: Multi-tenant SaaS (organization-based)
alter table documents enable row level security;

create policy "Users can access their org's documents"
on documents for all
using (
  organization_id in (
    select organization_id 
    from user_organizations 
    where user_id = auth.uid()
  )
);

-- Pattern 4: Role-based access
create policy "Admins can view all posts"
on posts for select
using (
  exists (
    select 1 from users
    where id = auth.uid()
    and role = 'admin'
  )
);

-- Pattern 5: Published content
create policy "View published or own drafts"
on posts for select
using (
  status = 'published' or 
  auth.uid() = author_id
);

Combining Multiple Policies

Multiple policies for the same operation are combined with OR logic. If ANY policy returns true, access is granted. This allows flexible access control like "users can view their own posts OR public posts OR posts in their organization." For restrictive policies, use a single policy with AND conditions.

sqlmultiple_policies.sql
-- Multiple SELECT policies (combined with OR)

-- Policy 1: View own posts
create policy "View own posts"
on posts for select
using ( auth.uid() = author_id );

-- Policy 2: View published posts
create policy "View published posts"
on posts for select
using ( status = 'published' );

-- Policy 3: Admins view all
create policy "Admins view all posts"
on posts for select
using (
  exists (
    select 1 from users
    where id = auth.uid() and role = 'admin'
  )
);

-- Result: Users can view posts if ANY condition is true:
-- - They own the post OR
-- - Post is published OR  
-- - They are an admin

Testing RLS Policies

sqltest_rls.sql
-- Test policies by impersonating users in SQL Editor

-- Method 1: Set JWT token claims
set request.jwt.claim.sub = 'user-id-here';

-- Now queries run as that user
select * from posts; -- Only returns posts accessible to that user

-- Reset to service role (see everything)
reset request.jwt.claim.sub;

-- Method 2: Test in JavaScript
const { data, error } = await supabase
  .from('posts')
  .select('*')

// RLS automatically applied based on logged-in user
// To test as different user, log in as that user

-- Method 3: Check policy definitions
select 
  schemaname,
  tablename,
  policyname,
  permissive,
  roles,
  cmd,
  qual,
  with_check
from pg_policies
where tablename = 'posts';

RLS Best Practices

  • Enable RLS on All Tables: Every table with user data should have RLS enabled immediately after creation
  • Start Restrictive: Begin with no access and explicitly grant permissions rather than allowing everything and restricting
  • Use Descriptive Policy Names: Name policies clearly describing what they allow (e.g., 'Users can view their own posts')
  • Test Policies Thoroughly: Test as different users to ensure policies work correctly before deploying
  • Index Foreign Keys: Policies using foreign keys benefit from indexes for performance
  • Document Complex Policies: Add SQL comments explaining complex policy logic
  • Avoid Overly Complex Policies: If policies become too complex, consider restructuring your data model

Common RLS Issues

  • Can't Query Data: RLS is enabled but no policies exist. Create appropriate policies to grant access
  • auth.uid() Returns NULL: User isn't authenticated or token expired. Check authentication status
  • Policy Not Working: Check policy applies to correct operation (SELECT vs INSERT vs UPDATE vs DELETE)
  • Performance Issues: Complex policies with subqueries can be slow. Add indexes and optimize queries
  • Service Role Bypasses RLS: Direct database connections with service_role key ignore RLS. Never use service_role in client apps
Pro Tip: Use Supabase's Table Editor to enable RLS and create common policy patterns with templates. For complex multi-tenant apps, consider adding an organization_id column to all tables and creating reusable policy functions. Test policies extensively in development before deploying to production.

Next Steps

  1. Implement in Projects: Add RLS to your existing tables following patterns from this guide
  2. Build Multi-Tenant Apps: Create organization-based policies for SaaS applications
  3. Add Advanced Logic: Use triggers and functions for complex authorization rules
  4. Complete Projects: Build secure apps with React CRUD tutorial or Next.js integration

Conclusion

Row Level Security is the cornerstone of Supabase application security, providing database-level access control that cannot be bypassed from client applications. By leveraging PostgreSQL's RLS with Supabase authentication, you create bulletproof security where users can only access data they're authorized to see, modify, or delete. The combination of auth.uid() for user identification and flexible policy expressions enables everything from simple single-user access to complex multi-tenant architectures with role-based permissions. Always enable RLS on tables containing user data, start with restrictive policies and explicitly grant permissions, test thoroughly as different users, and maintain clear policy documentation. With RLS mastered, your Supabase applications have enterprise-grade security suitable for production use with sensitive data. Continue building secure, scalable applications by combining RLS with secure file storage, database automation, and production integrations.

$ cat /comments/ (0)

new_comment.sh

// Email hidden from public

>_

$ cat /comments/

// No comments found. Be the first!

[session] guest@{codershandbook}[timestamp] 2026

Navigation

Categories

Connect

Subscribe

// 2026 {Coders Handbook}. EOF.