Supabase Row Level Security: Five Policy Examples
Supabase row level security (RLS) uses Postgres policies to decide which rows each request can read or write. You enable RLS on a table, then add policies that compare the row to the signed-in user, usually through auth.uid() or a claim in the JWT.
Below are five patterns that cover most apps, how to test them, and the mistakes that quietly expose data.
Vendors Covered in this Article
Disclosure: We may earn a commission if you buy through some links on this page. It doesn't change what we recommend.
How do you turn on RLS and write the first policy?
RLS is off until you enable it on each table. Tables created from SQL don't turn it on for you, so check every table in the exposed schema. Once it's enabled with no policies, nobody except the service role can read or write rows, which is the safe default.
- Enable it: alter table notes enable row level security.
- Add a select policy for the authenticated role: using (user_id = (select auth.uid())).
- Add an insert policy with a check clause: with check (user_id = (select auth.uid())).
- Add update and delete policies with the same ownership condition.
- Test as an anonymous user and as two different signed-in users.
Two clause types matter. USING filters which existing rows a request can see or modify. WITH CHECK validates the new values being written. An update policy usually needs both, or a user could change a row's owner to someone else. Wrapping auth.uid() in a select is a commonly recommended way to let Postgres evaluate it once per statement rather than per row.
Example patterns you can adapt
These five cover most products:
- Owner-only rows. Each row has a user_id; policies compare it to the current user. Fits notes, drafts, personal settings.
- Team or organization membership. A memberships table links users to organizations. A row policy checks whether the current user has a membership in the row's org_id, using an exists subquery. Fits multi-tenant SaaS.
- Public read, owner write. A select policy for everyone (or anon and authenticated) with insert, update and delete restricted to the owner. Fits published profiles or posts.
- Role-based access. Read the role from a claim you control, such as app_metadata in the JWT, and allow admins broader access. Never base a policy on user_metadata, which users can edit themselves.
- Shared records with a join table. A document is visible if the user appears in a document_shares table for it. Fits collaborative documents.
For the membership pattern, put an index on the columns the policy filters, such as memberships.user_id and org_id, because the subquery runs for many rows.
How to test that policies work
Policies fail silently: a wrong policy usually returns zero rows, not an error, and a too-open policy returns everything. Test both directions.
- In the SQL editor, set the role to authenticated and set the JWT claims to a test user's ID, then run selects and writes and confirm only expected rows come back.
- Repeat with a second user and with the anon role.
- From your app's client library, try to read and update another user's record by ID. The result should be empty or rejected.
- Check writes as well as reads: try inserting a row with someone else's user_id.
- Add these checks to your test suite so a later migration can't loosen a policy unnoticed.
Remember the service role key bypasses RLS entirely. It belongs only in trusted server code, never in a browser or mobile app.
Mistakes that expose data
The usual causes of RLS incidents:
- Forgetting to enable RLS on a new table, especially helper and join tables.
- Views. A view can run with its owner's rights and skip the underlying table's policies unless you configure it to run as the invoking user (available in newer Postgres versions). Check how each view is defined.
- Functions marked security definer that read tables without checking who's calling.
- Policies that use user-editable metadata for authorization.
- Storage buckets and realtime channels, which have their own access rules and aren't covered by table policies.
Treat a cross-tenant read as a top-severity event in your incident severity levels.
When RLS is not enough on its own
RLS is a strong last line of defense, but it doesn't replace application checks, rate limits or input validation. It can also become hard to reason about when many policies stack up. Keep policies short, name them clearly, and store them in migration files so changes go through review. If you're outgrowing the hosted platform, consider the tradeoffs in MongoDB Atlas vs AWS RDS vs Supabase and, for planning a move, the Supabase to RDS migration guide. Backups matter too: see the Postgres backup checklist.
What Good Looks Like
Every table in an exposed schema has RLS enabled, with policies covering select, insert, update and delete, and automated tests that prove one user can't touch another's rows.
Building The Capability (5-Stage Skill Ladder)
How to Get Started
Disclosure: We may earn a commission if you buy through some links on this page. It doesn't change what we recommend.
Frequently Asked Questions
What happens if RLS is enabled but no policy exists?
Every request that uses the regular client roles returns no rows and rejects writes, because access is denied by default. The service role still bypasses RLS, so use it only in trusted server code.
What is the difference between USING and WITH CHECK?
USING decides which existing rows a request may see or change. WITH CHECK validates the values of rows being inserted or updated. Update policies typically need both so users can't reassign rows.
Can I use user_metadata in a policy?
Avoid it. Users can edit their own user_metadata, so a policy based on it can be bypassed. Store authorization data in app_metadata or in a database table that only your server can change.
Does RLS slow down queries?
It can, because policy conditions run with each query. Index the columns used in policies, keep subqueries simple, and wrap auth.uid() in a select so it's evaluated once. Measure with real data volumes.
About the numbers
This guide doesn't quote a sourced benchmark. Figures in it are estimates or general guidance, so check them against your own numbers.
Related Guides
Defining Incident Severity Levels: A Four-Tier Template
Define SEV1 to SEV4 by customer impact, with response expectations, who can declare and change severity, and mistakes that cause false alarms.
MongoDB Atlas vs AWS RDS vs Supabase: Managed Database Comparison
Compare MongoDB Atlas, AWS RDS, and Supabase for managed databases: document vs relational schemas, automated backups, developer velocity, and cloud cost.
Migrating From Supabase to Amazon RDS: A Planning Guide
Plan a move from Supabase to Amazon RDS: what Supabase gives you beyond Postgres, extension and role checks, dump and restore, low-downtime cutover.
Postgres Backup Checklist: What to Verify Before You Need It
A Postgres backup checklist covering logical dumps, point-in-time recovery, retention, off-account copies and the restore drill most teams skip.
Cursor Rules Files: Examples for Next.js, Python and SQL Repos
Sample Cursor rules for a TypeScript app, a Python service and a SQL migrations folder, plus how to write rules the model actually follows.
Disaster Recovery Plan: Setting RTO and RPO per Service
Build a disaster recovery plan by tiering services, setting RTO and RPO for each, choosing a recovery pattern and running a test you can trust.