← Back to blog

Supabase security

A practical RLS authorization test matrix for Supabase

An RLS policy can look right in SQL and still allow the wrong user to act. The most useful test is the same request as somebody else.

I use a simple fixture: user A owns record A, user B owns record B, and an anonymous client has no session. Run this matrix against staging with harmless records. Write down the expected result before you run it. This makes a failed test harder to rationalize away.

Copy this matrix for each private resource

“Allow” assumes users may manage their own records. Replace each expectation with your actual product rule. Check both the response and the resulting database state. A 2xx response alone does not prove the intended row changed.

Actor and targetSELECTINSERTUPDATEDELETE
No session → A's private rowDeny or emptyDenyDenyDeny
User A → A's rowAllowAllow with A as ownerAllowAllow
User A → B's rowDeny or emptyDeny spoofed owner BDenyDeny
User B → A's rowDeny or emptyDeny spoofed owner ADenyDeny
Privileged server processOnly what its job requiresSameSameSame

If your app has organizations, add a second tenant. A user in tenant 1 must not see tenant 2's records just because both users share a role. Add admin and support roles separately. That turns “we have RLS” into explicit authorization promises. The approach follows OWASP's authorization regression testing guidance.

When a cell fails

First confirm the database role and user identity the request used. A client can use a publishable or legacy anon key, but a signed-in session changes the effective role to authenticated. Do not test cross-user isolation with a privileged secret or legacy service-role key; those bypass RLS. Supabase explains this in its API-key guide.

Next, check both database gates. Grants decide whether a role can use a table or function. RLS restricts rows for table operations. With RLS enabled and no applicable policy, access is denied by default. With RLS disabled, a broad grant can expose every row. Policies may differ for SELECT, INSERT, UPDATE, and DELETE, including USING and WITH CHECK conditions. See Supabase's RLS guide and API security guide.

Check paths around the table

SurfaceQuestionEvidence to keep
ViewCan it return a row the caller cannot read from the table?Definition, caller role, sanitized response
RPC functionCan anonymous or signed-in users run elevated code?EXECUTE grants, function definition, request and response
StorageCan A read or overwrite B's object?Bucket policy, object path, request and response
App API routeDoes the server check the session and owner before using a privileged key?Route code and two-user HTTP test

Views may run with their creator's permissions; for PostgreSQL 15 and newer, check whether security-invoker behavior is appropriate. Functions need an explicit review of EXECUTE grants and SECURITY DEFINER code. RLS on a table is not a blanket rule for those paths. See Supabase's view guide.

Keep the test after the fix

Save the fixture IDs and assertions in your test suite. Run them after policy, table, or API-route changes. Reset staging records between runs so an earlier mutation cannot make a later denial look like a pass. Keep allowed and denied cases together: a policy that blocks everything is not a working product, and one that allows everything is not security.

A live scan can probe public reachability and some authenticated paths when you provide test credentials. It cannot prove every tenant, role, and write operation is correct. Use this matrix for those product-specific rules. Our RLS setup guide covers the policy side, and our public-key guide explains why a key in your bundle is not itself an exposure.

Want an outside view of the deployed app? Run a scan and compare the results with your own two-user tests.