Supabase

Supabase RLS policies, with SQL you can use and test.

Supabase row-level security controls which rows each user can read and write. The safe pattern is simple: enable RLS on every exposed table, add a policy for each allowed operation, then test those rules with two ordinary accounts. The examples below show the SQL.

Get a quote

The anon key is public. RLS is what makes that safe.

Every Supabase project has a public browser credential: an sb_publishable_ key on newer projects or a legacy anon key on older ones. What stops that credential from becoming a master key is row-level security. With RLS off, the public API can reach the table. With correct policies, it can only do what you explicitly allow.

Step one: enable RLS and start closed

Enable RLS on every table exposed through the Supabase Data API. With RLS enabled and no policies, requests through the anon or authenticated role are denied. That closed starting point is safer than leaving a table open while you work out its rules.

Do not skip join tables, settings tables or lookup tables because they look harmless. Those tables often contain account identifiers, internal states or relationships that make another exposure easier to use.

alter table public.todos enable row level security;

Step two: add owner-scoped policies

A normal multi-tenant table stores the owner's user id in a column such as user_id. The select policy below returns only rows owned by the signed-in user. The insert policy uses WITH CHECK because it validates the new row being created.

Use USING for rows the user may act on. Use WITH CHECK for the new version of a row after an insert or update. An update commonly needs both, and it also needs a select policy so the row can be found in the first place.

create policy "Users can read their own todos"
on public.todos for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "Users can create their own todos"
on public.todos for insert
to authenticated
with check ((select auth.uid()) = user_id);
create policy "Users can update their own todos"
on public.todos for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
create policy "Users can delete their own todos"
on public.todos for delete
to authenticated
using ((select auth.uid()) = user_id);

Step three: separate public reads from writes

Public content should usually get a narrow select policy, not a blanket policy covering every operation. This example lets anonymous and signed-in visitors read published posts while leaving inserts, updates and deletes closed unless separate policies allow them.

Avoid USING (true) on private tables. The dashboard will report that RLS is enabled, but the policy still makes every row public to anyone holding the publishable key.

create policy "Anyone can read published posts"
on public.posts for select
to anon, authenticated
using (published = true);

Step four: test with two accounts and the public key

Create two ordinary users. Add records under each account, then try to read, update and delete the other user's records. Run the same checks through the API instead of trusting what the interface hides. A frontend filter is not an access rule.

Also query every exposed table without a login using the publishable or legacy anon key. The expected result is either public data you meant to expose or no rows. Private customer data should never come back to a bare public key.

Functions, storage and third-party authentication

RLS protects table queries, but SECURITY DEFINER functions can run with their owner's privileges. Any function callable from the browser needs its own authorization check, especially functions that mint tokens, change roles or write across accounts. Storage buckets need matching object policies too.

Third-party authentication is another common failure point. If users sign in through Clerk, NextAuth or another provider, confirm Supabase receives a token it can map to the user in your policies. Turning RLS off or moving a service-role key into the client may make the app work again, but it removes the boundary between every account.

The service-role or secret key bypasses RLS by design. Keep it in trusted server code. If it appears in a browser bundle, rotate or replace it and review what the exposed credential could reach.

Common questions.

What is an RLS policy in Supabase?
An RLS policy is a PostgreSQL rule that decides which rows a database role may read, insert, update or delete. Supabase applies those rules to requests made with the publishable or legacy anon key, including requests from signed-in users.
Do I need RLS if my app already filters data in code?
Yes. Application filtering is bypassed the moment someone queries Supabase directly with the anon key, which is public. RLS is enforced by the database itself, so it holds even when the application code is bypassed. The two are not substitutes; RLS is the one that actually protects the data.
What is auth.uid() in a policy?
It is the Supabase helper that returns the id of the currently authenticated user. Comparing a row's user_id to auth.uid() is the standard way to say a user may only touch their own rows.
What is the difference between USING and WITH CHECK?
USING decides which existing rows a user may read or act on. WITH CHECK validates the new row created by an insert or the new version written by an update. Owner-scoped update policies normally use both.
Should I disable RLS while debugging?
No. Disabling RLS can make a broken query start working by opening the table to the public API. Keep RLS enabled and fix the policy or authentication token that is blocking the intended request.
Can you audit my Supabase policies?
Yes, that is one of our most common audits. We check RLS on every table, review your functions and key handling, and give you the correct policy for anything left open.

Related services.

Let's build something real.

Tell us about your app or idea. You'll get a clear plan and a fixed quote back within 24 hours.

Get a quote