Securing Your Supabase API with Row Level Security (RLS)
Stop writing middleware for basic authorization. Learn how to use Supabase Row Level Security (RLS) to move your access control directly into Postgres for a more secure, scalable API.
21 Mar 2026, 21:34 UTC

The Problem: Authorization Without a Middleware Server
In traditional application architectures, you write a backend API that acts as a gatekeeper. When a user requests data, your server verifies their session, checks their permissions in a database, and then executes a filtered query. This "middleware" layer is where authorization lives.
Supabase changes this by exposing your Postgres database directly to the client via PostgREST. While this removes the need to write boilerplate CRUD endpoints, it introduces a critical risk: if you ship your anon key to a browser, any user could theoretically query any table. The solution is Row Level Security (RLS), a native Postgres feature that moves authorization from the application layer directly into the database engine.
How RLS Functions as a Security Boundary
By default, Postgres tables are open. When you enable RLS on a table, you flip the default behavior to "deny all." Once ALTER TABLE table_name ENABLE ROW LEVEL SECURITY; is executed, every request made via the Supabase API returns zero results unless a specific Policy explicitly allows it.
Policies are essentially WHERE clauses that the database automatically appends to every query. They rely on two primary expressions:
- USING: Controls which existing rows are visible (used for
SELECT,UPDATE, andDELETE). - WITH CHECK: Controls what data can be inserted or modified (used for
INSERTandUPDATE).
Because Supabase integrates with GoTrue (its auth provider), the database has access to the user's JSON Web Token (JWT). The helper function auth.uid() extracts the user's unique ID from this token, allowing you to tie data ownership to the authenticated session without passing user IDs manually in the client request.
Worked Example: Building a Secure Todo List
Consider a todos table where users should only see and edit their own tasks. To implement this, you must first enable RLS and then define policies for each CRUD operation.
Database Configuration
Run these commands in the Supabase SQL Editor (requires postgres or service_role permissions):
-- 1. Enable RLS
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;
-- 2. Allow users to view only their own todos
CREATE POLICY "Users can view their own todos"
ON todos FOR SELECT
USING (auth.uid() = user_id);
-- 3. Allow users to insert their own todos
CREATE POLICY "Users can insert their own todos"
ON todos FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- 4. Allow users to update only their own todos
CREATE POLICY "Users can update their own todos"
ON todos FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- 5. Allow users to delete only their own todos
CREATE POLICY "Users can delete their own todos"
ON todos FOR DELETE
USING (auth.uid() = user_id);
Client-Side Execution
Using the supabase-js client, you can now perform operations without worrying about filtering by user ID in the code. The database handles it automatically:
// This request will only return rows where user_id matches the logged-in user
const { data, error } = await supabase
.from('todos')
.select('*');
Performance and Trade-offs
RLS is powerful, but it is not free. Because the policy is evaluated for every row in a result set, complex policies can lead to performance degradation.
The Indexing Requirement
If your policy uses auth.uid() = user_id, Postgres must scan the user_id column for every request. On a table with millions of rows, a sequential scan will crash your API response times. You must create an index on the column used in your RLS predicates:
CREATE INDEX idx_todos_user_id ON todos(user_id);
The Service Role Exception
It is important to distinguish between the anon key and the service_role key. The service_role key bypasses RLS entirely. It is designed for administrative tasks in a secure backend environment (like an Edge Function or a Node.js server). If this key is accidentally leaked to the client, your RLS policies are effectively ignored, and your entire database is exposed.
Verification Checklist
To ensure your security boundary is intact, perform these three checks:
- The Zero-Result Test: Create a new table and enable RLS, but add no policies. Attempt a
SELECTvia the client SDK; it should return an empty array, not an error. - The Cross-User Test: Sign in as User A and attempt to
UPDATEa row belonging to User B using its ID. The request should fail or return 0 rows affected. - The Execution Plan: Run
EXPLAIN ANALYZEon a query in the SQL editor to verify that the query planner is using an Index Scan rather than a Seq Scan when applying the RLS filter.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.