Supabase Row Level Security: The Complete Guide for Developers
Learn how to use Supabase RLS to secure your data at the database level — no backend middleware required. Includes real-world policy examples for SaaS apps.
Row Level Security is the feature that makes Supabase remarkable. It lets you write SQL policies that control exactly who can see and modify which rows — enforced at the database level, before any application code runs.
This means zero middleware, zero authorization boilerplate, and security you can trust even if your application code has bugs.
What RLS Actually Does
Without RLS, every query returns everything in the table (assuming the user has table access). With RLS enabled, every query is silently filtered by your policies. A user querying the products table only sees rows where their user ID matches — automatically, invisibly, every time.
-- Enable RLS on a table
ALTER TABLE products ENABLE ROW LEVEL SECURITY;Once you run that command, the table returns zero rows to everyone by default. You then write policies to grant access back selectively.
The Four Policy Types
Supabase RLS has four policy types that map to SQL operations: SELECT, INSERT, UPDATE, DELETE. You write separate policies for each operation type.
SELECT Policy — Who can read rows?
CREATE POLICY "users see own data"
ON profiles FOR SELECT
USING (auth.uid() = user_id);USING defines the filter applied to reads. Only rows where auth.uid() equals user_id are returned.
INSERT Policy — Who can create rows?
CREATE POLICY "users insert own rows"
ON profiles FOR INSERT
WITH CHECK (auth.uid() = user_id);WITH CHECK validates new rows. The insert is rejected if the condition isn't met.
UPDATE and DELETE
Both use USING to filter which rows the operation applies to. You can also add WITH CHECK to UPDATE to validate the new values.
Real-World Pattern: Multi-Tenant SaaS
In a SaaS with organizations, users should only see data belonging to their organization. Here's the pattern:
-- Helper function for org membership check
CREATE OR REPLACE FUNCTION is_member_of(org_id uuid)
RETURNS boolean AS $$
SELECT EXISTS (
SELECT 1 FROM org_members
WHERE org_members.org_id = $1
AND org_members.user_id = auth.uid()
);
$$ LANGUAGE sql SECURITY DEFINER;-- Policy using the helper CREATE POLICY "org members see org data" ON projects FOR SELECT USING (is_member_of(org_id)); ```
The SECURITY DEFINER function runs with elevated privileges, which lets it query other tables (like org_members) even when the user doesn't have direct access.
Admin Bypass Pattern
You'll often need admin users to bypass RLS. The cleanest way is a custom claim in your JWT or a lookup in a profiles table.
CREATE OR REPLACE FUNCTION is_admin()
RETURNS boolean AS $$
SELECT COALESCE(
(SELECT is_admin FROM profiles WHERE id = auth.uid()),
false
);
$$ LANGUAGE sql SECURITY DEFINER;CREATE POLICY "admins see everything" ON products FOR ALL USING (is_admin()); ```
Common Mistakes
Forgetting to enable RLS: Tables without RLS enabled expose all data to anyone with the anon key. Always enable RLS when you create a table.
Using the anon key server-side: The service role key bypasses RLS intentionally — use it only in server-side code (Next.js API routes, webhooks). The anon key goes in client-side code and is subject to RLS.
Not testing with a real user token: Test your policies in the Supabase SQL editor using auth.uid() = 'your-user-uuid' to simulate different users.
Get Started
LaunchSrc has Next.js + Supabase starter templates with RLS already configured — auth policies, admin bypass patterns, and multi-tenant structures ready to customize.
Want the shortcut instead of another blank project?
Browse developer-ready prompts, Next.js templates, and source code you can use immediately.
Browse Prompts and Templates →