JustHandled Labs
// database security guide

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.

The short version

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.

OperationPolicy evidenceFailure to test
SELECTUSING restricts visible rows to the intended role and ownership boundary.User B can read User A's row; an anonymous request sees authenticated data.
INSERTWITH CHECK prevents creating a row for another user or tenant.User A supplies User B's owner or organization ID.
UPDATEUSING 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.
DELETEUSING 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.

IdentityExpected proofNegative proof
AnonymousOnly intentionally public rows and operations succeed.Private reads and every unauthorized write fail.
User AOwn rows and permitted shared rows work.User B's private rows remain unavailable.
User BThe same rules hold with different row ownership.Changing an owner or tenant identifier does not cross the boundary.
Trusted serverRequired 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

A

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.

B

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.

C

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.

D

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.

Primary references

Find the RLS review target before launch.

Supabase RLS Doctor creates an evidence-backed review queue from local migrations, policy files, client initialization, schema snapshots, and storage rules. It starts read-only and cannot certify the live project.

Get Supabase RLS Doctor on Agensi