Row Level Security (RLS) in Supabase isn’t just about SELECT * FROM users WHERE id = auth.uid(); it’s a fundamental architectural shift that lets you define data access rules directly within your database, making your application’s security inseparable from its data layer.
Let’s see RLS in action with a common scenario: users managing their own profiles.
-- Enable RLS for the 'profiles' table
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- Policy: Authenticated users can only see their own profile
CREATE POLICY "Users can view their own profile"
ON profiles FOR SELECT
USING (auth.uid() = id);
-- Policy: Authenticated users can update their own profile
CREATE POLICY "Users can update their own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);
-- Policy: Any user can create a profile (assuming this is handled by signup logic elsewhere)
CREATE POLICY "Allow authenticated users to insert their profile"
ON profiles FOR INSERT
WITH CHECK (auth.uid() = id);
In this snippet, auth.uid() is the magic. It’s a PostgreSQL function provided by Supabase that returns the ID of the currently authenticated user. The USING clause in SELECT and UPDATE policies acts as a WHERE clause for the entire query. The WITH CHECK clause on INSERT ensures that the id being inserted matches the authenticated user’s ID.
Now, let’s consider a more complex scenario: a blog where users can create posts, and only the author can edit or delete their own posts, while other users can only read them.
First, we need a posts table.
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
author_id UUID REFERENCES profiles(id) NOT NULL
);
And we need to enable RLS on it.
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
Here are the policies:
-
Public Read Access: Any user, authenticated or not, can read all posts.
CREATE POLICY "Public read access for posts" ON posts FOR SELECT USING (true);The
USING (true)here means "no conditions apply, anyone can read." -
Author Access for Updates/Deletes: Only the author of a post can update or delete it.
CREATE POLICY "Authors can update their own posts" ON posts FOR UPDATE USING (auth.uid() = author_id); CREATE POLICY "Authors can delete their own posts" ON posts FOR DELETE USING (auth.uid() = author_id);Notice that both
UPDATEandDELETEuse the sameUSINGclause. Theauth.uid()is compared against theauthor_idcolumn in thepoststable for the rows being affected. -
Authenticated User Post Creation: Only authenticated users can create posts, and they must be linked to their own profile.
CREATE POLICY "Authenticated users can insert their posts" ON posts FOR INSERT WITH CHECK (auth.uid() = author_id);This
WITH CHECKensures that when a user creates a post, theauthor_idassociated with that post is indeed their own authenticated ID.
Consider a multi-tenant application where different organizations have their own private data. Let’s say we have an organizations table and a projects table, where projects belong to a specific organization.
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL
);
CREATE TABLE projects (
id SERIAL PRIMARY KEY,
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE NOT NULL,
name TEXT NOT NULL,
description TEXT
);
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
Now, let’s define access policies. Assume users are associated with organizations via a separate organization_members table.
-- Users can only see organizations they are members of
CREATE POLICY "Members can view their organizations"
ON organizations FOR SELECT
USING (
EXISTS (
SELECT 1
FROM organization_members om
WHERE om.organization_id = organizations.id
AND om.user_id = auth.uid()
)
);
-- Users can only see projects within organizations they are members of
CREATE POLICY "Members can view projects in their organizations"
ON projects FOR SELECT
USING (
EXISTS (
SELECT 1
FROM organization_members om
WHERE om.organization_id = projects.organization_id
AND om.user_id = auth.uid()
)
);
-- Users can only create projects in organizations they are members of
CREATE POLICY "Members can create projects in their organizations"
ON projects FOR INSERT
WITH CHECK (
EXISTS (
SELECT 1
FROM organization_members om
WHERE om.organization_id = projects.organization_id
AND om.user_id = auth.uid()
)
);
-- Users can only update/delete projects in organizations they are members of
CREATE POLICY "Members can update/delete projects in their organizations"
ON projects FOR UPDATE, DELETE
USING (
EXISTS (
SELECT 1
FROM organization_members om
WHERE om.organization_id = projects.organization_id
AND om.user_id = auth.uid()
)
);
The EXISTS subquery is powerful. It checks for the existence of a record in organization_members that links the current user (auth.uid()) to the organization_id of the row being accessed in organizations or projects. This effectively scopes all data access to the user’s organizational membership.
One common pitfall is forgetting to enable RLS on a table. If RLS is disabled, no policies are enforced, and by default, all data is accessible. Another is confusion between USING and WITH CHECK. USING applies to SELECT, UPDATE, and DELETE queries to determine which rows are visible or modifiable. WITH CHECK applies to INSERT and UPDATE queries to ensure the new data being written conforms to the policy.
When you create a new role with specific permissions, you’ll often need to create corresponding RLS policies to grant access to that role for specific tables.