How to Set Up Supabase Row-Level Security for AI Applications
Why RLS Matters for AI Applications
AI applications often handle sensitive data: user documents, generated content, scoring results, and conversation histories. Row-level security (RLS) in Supabase ensures that users can only access their own data, even if there is a bug in your application code. It is defense in depth applied at the database layer.
I use RLS on every Supabase table in my AI projects. It adds a small amount of setup time but eliminates entire categories of data leak vulnerabilities.
How RLS Works in Supabase
RLS works by attaching policies to database tables. Every query is automatically filtered by these policies. If a user tries to select rows they do not own, those rows simply do not appear in the results. No application code needed.
The flow works like this:
- User authenticates via Supabase Auth and receives a JWT
- The JWT contains the user's ID and role
- Every database query includes this JWT
- PostgreSQL evaluates RLS policies using the JWT claims
- Only rows matching the policy are returned or modified
Setting Up RLS for a Content Scoring Table
Let me walk through a real example from my content scoring application. Here is the table structure:
CREATE TABLE scored_content (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users(id),
content TEXT NOT NULL,
score FLOAT,
model_used TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Enable RLS
ALTER TABLE scored_content ENABLE ROW LEVEL SECURITY;
Basic User Isolation Policy
The most common pattern: users can only see and modify their own rows:
-- Users can read their own scored content
CREATE POLICY "Users read own content"
ON scored_content
FOR SELECT
USING (auth.uid() = user_id);
-- Users can insert their own content
CREATE POLICY "Users insert own content"
ON scored_content
FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Users can delete their own content
CREATE POLICY "Users delete own content"
ON scored_content
FOR DELETE
USING (auth.uid() = user_id);
Service Role Access for AI Pipelines
Your AI pipeline backend needs to read and write data for any user. This is where the service role key comes in. The service role bypasses RLS entirely, which is exactly what you want for server-side processing:
from supabase import create_client
import os
# Client-side: uses anon key, subject to RLS
public_client = create_client(
os.environ['SUPABASE_URL'],
os.environ['SUPABASE_ANON_KEY']
)
# Server-side: uses service key, bypasses RLS
service_client = create_client(
os.environ['SUPABASE_URL'],
os.environ['SUPABASE_SERVICE_KEY']
)
Critical security note: Never expose the service role key to client-side code. It should only exist in your server environment variables.
Advanced Patterns for AI Applications
Shared Content with Visibility Controls
Some AI applications need content sharing. For example, a team might share scored documents. You can model this with a visibility column:
ALTER TABLE scored_content ADD COLUMN visibility TEXT DEFAULT 'private';
ALTER TABLE scored_content ADD COLUMN team_id UUID;
CREATE POLICY "Users read shared team content"
ON scored_content
FOR SELECT
USING (
auth.uid() = user_id
OR (
visibility = 'team'
AND team_id IN (
SELECT team_id FROM team_members
WHERE user_id = auth.uid()
)
)
);
Rate Limiting at the Database Level
You can use RLS policies to enforce rate limits on AI operations. This prevents a single user from overwhelming your scoring pipeline:
CREATE POLICY "Rate limit inserts"
ON scored_content
FOR INSERT
WITH CHECK (
auth.uid() = user_id
AND (
SELECT COUNT(*)
FROM scored_content
WHERE user_id = auth.uid()
AND created_at > NOW() - INTERVAL '1 hour'
) < 100
);
Common Pitfalls
After implementing RLS across multiple projects, here are the mistakes I see most often:
- Forgetting to enable RLS: Creating policies without enabling RLS on the table does nothing. Always run
ALTER TABLE ... ENABLE ROW LEVEL SECURITY - Missing policies for specific operations: A SELECT policy does not cover INSERT or UPDATE. You need separate policies for each operation type
- Performance issues with complex policies: Subqueries in RLS policies run on every row access. Keep policies simple and ensure referenced tables are indexed
- Testing with the service key: If you test with the service key, RLS is bypassed and everything works. Always test with the anon key to verify your policies
Testing RLS Policies
I test RLS policies systematically before deploying:
-- Test as a specific user
SET LOCAL role = 'authenticated';
SET LOCAL request.jwt.claims = '{"sub": "user-uuid-here"}';
-- This should return only that user's rows
SELECT * FROM scored_content;
-- This should fail
DELETE FROM scored_content WHERE user_id != 'user-uuid-here';
Row-level security is not optional for AI applications that handle user data. It is the single most effective way to prevent data leaks, and it costs almost nothing to implement correctly.
Getting Started
Start by enabling RLS on all your tables and adding basic user isolation policies. You can always add more sophisticated policies later. The important thing is that the foundation is in place from the beginning. Retrofitting RLS onto an existing application is much harder than building it in from the start.