Supabase

A practical guide to Supabase row-level security.

Supabase row-level security is how you control which rows each user can read and write. Enable it on every table, write a policy for each access path, and the anon key that connects your app can no longer reach data it should not. Here is how to do it correctly.

Get a quote

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

Every Supabase project ships with an anon key that is meant to live in your front-end code. It is not a secret. What keeps it from being a master key to your whole database is row-level security. With RLS off, the anon key reads and writes every table. With RLS on and correct policies, it can only do what you have explicitly allowed. Getting this right is the most important security step in a Supabase app.

Step one: enable RLS on every table

In Supabase, a new table has RLS disabled, which means it is fully open to the anon key. Turn it on for every table that holds real data, from the table editor or with alter table enable row level security. The moment you enable it with no policies, the table denies all access, which is the safe default. Now you add back exactly the access you intend.

Do not skip tables because they seem harmless. Join tables, settings tables and lookup tables have all turned out to leak more than expected. Enable RLS everywhere, then open up deliberately.

Step two: write a policy per access path

Each table needs policies for the operations it should allow. A typical owner-scoped table has a select policy allowing a user to read rows where user_id = auth.uid(), and matching insert, update and delete policies with the same condition. Public-read tables get a select policy with a using clause of true, but keep writes locked. Admin access is a separate policy checking a role.

The most common mistake is writing a select policy and forgetting the others, which leaves the table readable-safe but writable by anyone. Every operation you intend to allow needs its own policy; everything else stays denied.

Step three: lock down functions and the service role

Row-level security protects tables, but database functions can bypass it if they run as the definer. Any function callable with the anon key needs its own authorisation check inside it, especially functions that create records, mint tokens or change roles. Review every function you have exposed.

Keep the service role key on the server, always. It bypasses RLS entirely by design, so if it ever ships to the browser, every policy you wrote is void. It belongs in server-side code and environment variables, never in the front end.

Step four: verify from the outside

Policies are easy to get subtly wrong, so verify rather than assume. Log in as two different users and confirm neither can reach the other's rows through the app. Query each table directly with the anon key and confirm it returns only what it should. This outside-in check is exactly what our Supabase security audit runs across every table and function.

Common questions.

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.
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