Row-Level Security (RLS) is a PostgreSQL feature that enforces data access rules at the database level. For multi-tenant SaaS products, it's the most important security control you can implement. Here's how it works and how to use it correctly.
What Is Row-Level Security?
Without RLS, if User A can access your API endpoint for listing projects, they could potentially access ALL projects in your database (depending on how your API is built). RLS adds a second layer: even if your application code has a bug, the database itself will only return rows that the current user is authorized to see.
Think of it as: your application enforces business logic, your database enforces data isolation.
Enabling RLS in Supabase
Enable RLS on every table that contains user data: ALTER TABLE projects ENABLE ROW LEVEL SECURITY;. Once enabled, all queries return zero rows by default until you add policies that grant access.
Writing RLS Policies
A policy defines who can do what. For a multi-tenant SaaS where each row has an org_id:
- SELECT policy:
org_id = auth.jwt() ->> 'org_id'— users can only see rows from their organization - INSERT policy:
org_id = auth.jwt() ->> 'org_id'— users can only create rows in their organization - UPDATE policy: same — users can only update their own organization's data
- DELETE policy: same — users can only delete their own organization's data
RLS with Clerk Authentication
When using Clerk for authentication, set the Supabase JWT template to include the org_id from Clerk's organization metadata. This makes the organization ID available in your RLS policies via auth.jwt().
Testing RLS Policies
Always test RLS by attempting cross-tenant data access in your development environment. Create two test organizations, log in as user from Org A, and try to query data from Org B. The query should return zero results. If it returns data, your RLS policy is wrong.
Need RLS Implemented Correctly in Your SaaS?
I take 2 clients per month. Ship your SaaS in 2–4 weeks with a developer who has done it 350+ times.
Start on Fiverr →RLS and Performance
RLS policies add a WHERE clause to every query automatically. This means your org_id column must be indexed for good performance. Add: CREATE INDEX ON projects(org_id); for every table with RLS policies.