StackAttestTechnical Trust
Free tool

The Supabase RLS checklist

Row level security is what stands between a key that is public by design and the rows in your database. It is also per table and per command, which is why a project is usually part done rather than done. List your tables, work the same checks down each one, and see what is still open. This is a Supabase security checklist narrowed to the part that decides who can read what. It runs in your browser, and nothing is sent anywhere.

0%
0 of 15 checks done, across 2 tables
0 of 6
0 of 6
Once for the whole project
What is still open
profiles
  • Row level security is enabled on the table
  • The SELECT policy scopes rows to their owner, not to anyone signed in
  • The owner is taken from the session, never from the request
  • INSERT has a policy of its own, with a with check expression
  • UPDATE has a policy with both using and with check
  • DELETE has a policy of its own
posts
  • Row level security is enabled on the table
  • The SELECT policy scopes rows to their owner, not to anyone signed in
  • The owner is taken from the session, never from the request
  • INSERT has a policy of its own, with a with check expression
  • UPDATE has a policy with both using and with check
  • DELETE has a policy of its own
Whole project
  • Storage buckets have policies, and a private bucket is actually private
  • Every security definer function has been read line by line
  • Ownership is enforced in the database or on a server, not by the frontend query

A ticked checklist is a record of what you believe about your own project. If you need something a third party can check, a Supabase RLS audit reads the policies your migrations declare and tests what the running system does when one account asks for another account's rows. The free security check needs only your address and no account, and it names the controls a public URL cannot reach rather than passing them.

Why the list is per table

There is no single place to check Supabase RLS and be finished, because the setting is per table and the policies under it are per command. While you are the only account in the database, a wrong policy and a right one look identical. Everything you can see is yours either way. The difference only appears once a second person has an account, and by then the table you forgot has been readable for as long as it has existed.

Row level security is also not one switch. A table can have it enabled and no policy, which denies everything and breaks the feature. It can have a policy that returns every row. It can have a correct read rule and nothing at all on the write commands, which is the common half-done case, because a missing write rule shows up as a broken button rather than as a leak. Each of those is a different state and only a per table, per command pass finds them.

Include the tables you created in the dashboard by hand, not only the ones your migrations declare. They are exposed through the same API and they are the ones nobody reviews.

The three that are not per table

Storage buckets. Files live under the same rules as rows. A public bucket serves any object to whoever has the URL, which is correct for avatars and wrong for anything a customer uploaded. For a private bucket, the policies on storage.objects are the only thing in the way, and they are per command in exactly the way a table's are.

Security definer functions. A function declared security definer runs with the rights of whoever created it. The caller's policies are not applied inside it, so everything it returns has already left row level security behind. That is sometimes exactly what you want, and it means the body has to do the scoping itself, and its search_path has to be pinned so the objects it names cannot be shadowed.

Filtering versus enforcing. A frontend query that filters on the current user's id looks like a restriction and is not one. The filter is part of the request, so it belongs to the caller, and removing it is a one-line change to code that already runs on their machine. Enforcement has to live somewhere they cannot edit: in the policy, or on a server.

Common questions

What should be on a Supabase RLS checklist?

Per table: row level security enabled, a SELECT policy that scopes rows to their owner rather than to anyone signed in, an owner derived from the session rather than from the request, and separate INSERT, UPDATE and DELETE policies. Once for the project: storage bucket policies, a read of every security definer function, and ownership enforced on the server rather than by a filter in the frontend query.

Is enabling row level security enough on its own?

No. Enabling it means policies are consulted. If the policy behind it evaluates to true, or only asks whether the caller is signed in, every row is still readable by anyone who can create an account. Enabled and scoped are different claims and only the second one protects anything.

Why does the checklist ask about INSERT, UPDATE and DELETE separately?

Policies in Postgres are per command. A correct SELECT policy and no rule at all on the write commands is the common half-done case, and the usual fix, one policy written for all commands, turns the read rule into the write rule. An UPDATE policy also needs a with check clause, or a caller can move a row out of their own scope.

Does filtering by user id in my query keep other users out?

No. The filter travels inside the request, so it is under the caller's control, and removing it is a one-line edit to code already running on their machine. It shapes what your application asks for, not what the database is willing to answer.

Why do security definer functions need their own review?

A security definer function executes with the rights of whoever created it, so the caller's policies do not apply inside it. Anything it returns has bypassed row level security, which means the function body has to do the scoping itself and its search_path has to be pinned.

Is this checklist free, and does it send my data anywhere?

It is free, needs no account, and runs entirely in your browser. Table names you type stay on your machine, nothing is stored, and nothing is sent to StackAttest or anyone else.

When a checklist stops being enough

This page records what you believe about your own project. That is worth having, and it is not evidence. If you need a result a third party has reason to believe, a Supabase RLS audit reads the policies your migrations declare, table by table and command by command, and tests what the deployed system actually does when one account asks for another account's rows.

If you want something back in the next minute instead, the free AI app security check needs only your address and no account. It cannot see a policy, so it reports the controls a public URL cannot reach as uncovered rather than passing them.

Keep reading