Supabase makes a secure design possible, but it also makes it easy to build a convincing interface before the access model has been tested. The dashboard shows the right records, so the team assumes the database would reject anything else.

Test row-level security as a database boundary. The interface cannot prove which rows the database API will return.

A missing policy and a broad policy fail differently

When RLS is enabled and no applicable policy exists, access is denied. That failure is visible and usually gets fixed quickly.

A broad policy is more dangerous because the application appears to work. A condition such as auth.uid() IS NOT NULL proves only that somebody signed in. It does not prove that they own the row, belong to the organisation or hold the right role.

Read each policy aloud as a sentence. “Any signed-in user can select rows where the account membership contains their user ID” is meaningful. “Any signed-in user can select” should trigger a deliberate business decision, not pass unnoticed.

Do not accept ownership from the request

A common insert flow sends user_id or account_id from the browser. If the policy merely checks that the column is non-empty, a user can create a record owned by somebody else or attach it to another account.

The policy should derive identity from the authenticated database context and compare it with trusted membership data. Where possible, set ownership on the server or in a database function rather than asking the browser to state who it is.

The same principle applies to updates. A USING expression decides which existing rows can be targeted. A WITH CHECK expression decides what the row may look like after the change. Missing the second check can allow a user to move a row into an account they do not control.

Service-role keys change the threat model

The Supabase service role bypasses RLS. That is useful for trusted server jobs and dangerous in a browser bundle, desktop client or loosely protected edge function.

If a service-role credential has ever reached client code, rotate it. Then inspect the functions and logs that used it. Investigate every action and data set the key allowed, including paths the normal interface never exposed.

Server code using the service role must implement its own authorisation before querying. “RLS protects the table” is no longer true on that path.

Test the negative cases

Teams naturally test that Alice can read Alice's project. The security evidence comes from showing that Alice cannot read Bob's project, cannot insert into Bob's account, cannot change ownership and cannot call a privileged function directly.

Create a small matrix:

Action Owner Same organisation Different organisation Anonymous
Select allow business decision deny deny
Insert allow with trusted owner business decision deny deny
Update allow bounded fields business decision deny deny
Delete explicit decision explicit decision deny deny

Run the matrix against the database API, not only through the screen. Keep those tests near the migrations so a later policy change cannot quietly reopen access.

A useful review output

The report should name the table, operation, role and policy involved. It should show the request made as one user and the record returned from another account, using safe test data. The fix should include the policy boundary and a regression test.

That is more valuable than saying “enable RLS.” Many serious Supabase access failures happen after RLS was enabled, inside a policy that expressed the wrong business rule.