Databases and resilienceSetup guide3 min readUpdated September 2026

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.

  1. Enable it: alter table notes enable row level security.
  2. Add a select policy for the authenticated role: using (user_id = (select auth.uid())).
  3. Add an insert policy with a check clause: with check (user_id = (select auth.uid())).
  4. Add update and delete policies with the same ownership condition.
  5. 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.

Executive Capability Standard

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)

1. Learn:Read how each of your tables is exposed through the API and list which ones have RLS on.
2. Do Manually:Write owner-only policies for one table, then test as two users and as anon in the SQL editor.
3. Delegate:Assign a reviewer for every migration that touches policies or adds a table.
4. Automate:Add tests that run cross-user reads and writes against a test database on every pull request.
5. Buy:Add a security scanner that flags tables without RLS and permissive policies across projects.

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.

Supabase

Fits when you want Postgres policies enforced at the database behind auto-generated APIs.

Visit Supabase→

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