Supabase RLS audit checklist before launch
Prove who can read and change each row before production traffic arrives. Enabling RLS is necessary; the release decision depends on role-by-role tests, elevated-key boundaries, views, storage rules, and migration evidence.
Inventory every table in every exposed schema. Confirm RLS is enabled, then test anon, an authenticated owner, a different user, and the trusted server path for every relevant operation. Review both USING and WITH CHECK, keep secret or service_role keys out of public clients, inspect views and security-definer functions, test storage.objects rules separately, and compare migrations with the deployed project. A clean linter or Security Advisor result helps prioritize review; it does not replace negative access tests.
Audit path
1. Inventory the actual exposed surface
Supabase states that RLS must be enabled on tables in exposed schemas; public is exposed by default. Tables created in the Dashboard Table Editor get RLS by default, but tables created through raw SQL or the SQL editor need an explicit check.
Start with an object inventory rather than the policies you remember writing. Include tables, views, functions, grants, and storage policies from migrations and schema snapshots. Record the intended audience for each object: public, signed-in users, row owner, organization member, staff, or server-only.
Release evidence
- Every table in every exposed schema has an owner and intended audience.
- Each table records whether RLS is enabled and which operations are intentionally available.
- Unexpected grants, views, or functions have an explicit disposition.
- The inventory comes from current schema or migration evidence, not a screenshot alone.
2. Review what each policy permits
Think of a policy as an implicit filter added to a query. An enabled RLS flag with an overly broad expression still exposes rows. Review the role in the TO clause, the operation, the ownership expression, and the difference between existing-row and new-row checks.
| Operation | Policy evidence | Failure to test |
|---|---|---|
| SELECT | USING restricts visible rows to the intended role and ownership boundary. | User B can read User A's row; an anonymous request sees authenticated data. |
| INSERT | WITH CHECK prevents creating a row for another user or tenant. | User A supplies User B's owner or organization ID. |
| UPDATE | USING controls target rows and WITH CHECK controls the resulting row. Supabase also notes that UPDATE needs a corresponding SELECT policy. | A user changes the ownership column or updates a row they cannot select. |
| DELETE | USING restricts the rows that can be deleted. | A user deletes another user's record or a shared administrative row. |
Treat USING (true) or WITH CHECK (true) as a deliberate public-access decision, not a harmless default. Also avoid using mutable user metadata for authorization; Supabase distinguishes user-editable raw_user_meta_data from raw_app_meta_data, which users cannot update themselves.
3. Test identities, operations, and negative cases
The useful test is not “can the owner read the row?” It is the matrix of who must succeed and who must fail. Run the same operation with the public client path your application actually uses.
| Identity | Expected proof | Negative proof |
|---|---|---|
| Anonymous | Only intentionally public rows and operations succeed. | Private reads and every unauthorized write fail. |
| User A | Own rows and permitted shared rows work. | User B's private rows remain unavailable. |
| User B | The same rules hold with different row ownership. | Changing an owner or tenant identifier does not cross the boundary. |
| Trusted server | Required administrative work succeeds only in the backend path. | No elevated key or bypass role appears in a browser bundle, mobile app, public repository, or client-readable configuration. |
Supabase supports client-level tests and database tests with the CLI and pgTAP. Keep tests beside the migrations so a policy change can fail CI before it reaches production.
supabase db reset supabase test db supabase db lint
Run those against a local development stack. Do not add --linked to a reset: Supabase documents db reset --linked as destructive and appropriate only for an intentional development or staging rebuild.
4. Separate public keys from RLS-bypassing keys
Current Supabase documentation distinguishes low-privilege publishable keys from elevated secret keys. Legacy anon and service_role keys map to the same public-versus-elevated split. Publishable or anon keys can be used in public clients when authorization is enforced by RLS. Secret and service_role keys bypass RLS and must stay in trusted backend components.
- Search client initialization, environment examples, generated bundles, logs, and deployment configuration for elevated key formats or legacy service-role references.
- Do not “test” public access with an elevated client; a successful request says nothing about the RLS policy.
- Use separate clients for user-session behavior and server administration. Confirm which Authorization token is actually sent.
- Rotate any elevated key that was exposed; removing it from the latest commit does not invalidate copies already distributed.
5. Check the surfaces outside ordinary tables
Views
Supabase warns that views bypass RLS by default because they are commonly created with a security-definer owner. On Postgres 15 and later, security_invoker = true can make a view obey underlying RLS for anon and authenticated. Otherwise revoke public roles or move the view to an unexposed schema.
Security-definer functions and bypass roles
A security-definer function runs with its creator's privileges. Supabase recommends keeping such functions out of exposed schemas. Inventory every function used by a policy and every role with bypassrls.
Storage
Supabase Storage access is controlled with policies on storage.objects. Test list, read, upload, overwrite, and delete separately; upsert needs additional SELECT and UPDATE permissions. A public bucket is intentionally public, so do not mistake a bucket flag for an ownership rule.
Column-sensitive data
RLS limits rows, not individual columns. If a permitted row contains secrets or restricted attributes, separate that data into an appropriately protected table or use deliberate column privileges after reviewing their tradeoffs.
6. Compare declared policy with deployed state
Migrations are reviewable evidence, but production can drift. Supabase notes that some entities, including policy renames and certain view properties, do not always diff cleanly. Compare migration history, schema snapshots, storage configuration, and the deployed project's Security Advisor findings.
Minimum release record
- Object inventory and intended access model.
- RLS enabled state and policy names for every exposed table.
- Allowed and denied results for the identity-operation matrix.
- Elevated-key search and rotation status.
- View, function, role, storage, and grant review.
- CLI test and lint output plus current Security Advisor disposition.
- Known gaps, reviewer, date, and the exact migration or commit approved.
Security Advisor can flag missing or improperly configured RLS, but no automated check can prove your business authorization model. Treat a clean result as one input to review, not a security certification.
Turn migration review into a repeatable audit
Supabase RLS DoctorRead-only review of SQL migrations, policy files, Supabase client initialization, schema snapshots, and storage policy configuration. It ranks evidence-backed review targets and never connects to or changes the live project.See the RLS audit skill →Common questions
How do I check whether RLS is enabled everywhere it should be?
Build an inventory of every table in each exposed schema, then record the row-security state beside the intended audience and operations. Do not assume raw-SQL migrations inherited the Dashboard's defaults.
Does enabling RLS prove a table is secure?
No. Test every policy by role and operation, including cross-user negative cases. Review keys, grants, views, functions, storage, and deployed drift as separate boundaries.
Can the publishable or anon key appear in client code?
Supabase documents publishable and legacy anon keys as low-privilege client keys whose data access is constrained by RLS. Secret and legacy service-role keys are elevated, bypass RLS, and must remain in trusted backend code.
Should I test policies with the service role?
Not as proof of user authorization. Elevated clients bypass RLS. Use the same public client and user sessions as the application for policy tests, then test the trusted server path separately.