Supabase · Row Level Security

Supabase RLS security checklist

Review Supabase Row Level Security with an evidence-led checklist for grants, policies, tenant boundaries, views, functions, claims, and authorized role tests.

Published · Updated · Reviewed by SafeForProd

A Supabase publishable key is safe in the browser only because data access is enforced elsewhere. Review Postgres grants and Row Level Security for every object exposed through the Data API, then test each intended role and operation with authorized accounts. Do not treat “RLS enabled” as proof that the policy matches the application’s tenant and ownership rules.

This checklist is for project owners and authorized reviewers. It is not a recipe for testing someone else’s Supabase project with a key found online.

The two layers to review

Supabase’s Data API security guide describes two separate controls:

  1. Postgres grants decide whether anon, authenticated, or service_role can reach a table, view, sequence, or function.
  2. RLS policies decide which rows an allowed role can read or modify.

Use both. RLS does not make an unnecessary grant desirable, and a narrow grant does not replace row-level rules.

1. Inventory the exposed surface

Start from the schemas configured as exposed in the project’s API settings. For each schema, inventory:

  • tables and partitioned tables;
  • views and materialized views;
  • functions reachable by Data API roles;
  • sequences used by insert paths;
  • Storage policies if the application uses Supabase Storage;
  • Realtime paths and server functions that depend on the same data.

If the application does not use the Data API, Supabase documents an option to disable it. If it does use the Data API, a dedicated exposed schema can make the intended API surface easier to audit than placing internal and public objects together.

An owner can use a read-only catalog query to list table RLS state:

select
  n.nspname as schema_name,
  c.relname as table_name,
  c.relrowsecurity as rls_enabled,
  c.relforcerowsecurity as rls_forced
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname in ('public')
  and c.relkind in ('r', 'p')
order by 1, 2;

Change the schema list to match the project’s exposed schemas. Save the output as review evidence; do not assume public is the only exposed schema.

2. Review grants before policies

List the privileges assigned to the client roles:

select
  table_schema,
  table_name,
  grantee,
  privilege_type
from information_schema.role_table_grants
where table_schema in ('public')
  and grantee in ('anon', 'authenticated', 'service_role')
order by 1, 2, 3, 4;

For each object, ask:

  • Does anon need any access?
  • Does authenticated need every granted operation?
  • Can the client call functions that were intended only for backend jobs?
  • Are new tables and functions receiving automatic default privileges?
  • Can internal tables move to an unexposed schema?

Supabase’s current guidance is to grant the minimum privilege each role needs and pair grants with RLS in the same migration.

3. Confirm RLS is enabled everywhere it must be

Supabase says RLS must be enabled on tables in an exposed schema. Tables created through the dashboard have RLS enabled by default; tables created through raw SQL or another tool may require an explicit statement:

alter table public.projects enable row level security;

Once RLS is enabled, the publishable key alone cannot access table data until an applicable policy permits it. That fail-closed starting point is useful, but it still needs policies that represent the real application rules.

Record a Confirmed finding when deterministic source or catalog evidence shows an exposed table with applicable client grants but without RLS or an equivalent control. If the live URL only reveals that the app uses Supabase, RLS remains Not tested.

4. Review policies by operation and role

List current policies:

select
  schemaname,
  tablename,
  policyname,
  roles,
  cmd,
  qual as using_expression,
  with_check
from pg_policies
where schemaname in ('public')
order by 1, 2, 4, 3;

Build a matrix for every table:

RoleSELECTINSERTUPDATEDELETE
anonIntended rows or deniedIntended rows or deniedIntended rows or deniedIntended rows or denied
authenticatedTenant/owner ruleNew-row ruleExisting- and new-row ruleExisting-row rule

Check the policy mechanics:

  • SELECT uses USING to decide which existing rows are visible.
  • INSERT uses WITH CHECK to validate a proposed row.
  • UPDATE commonly needs both USING for the existing row and WITH CHECK for the resulting row. Supabase also notes that an update requires a corresponding select policy.
  • DELETE uses USING to decide which existing rows may be removed.
  • The TO clause should name the intended Postgres roles rather than evaluating the policy for roles that never need it.

A broad policy such as USING (true) can be correct for deliberately public data. The evidence should include why that table and operation are public, not merely the SQL text.

5. Trace tenant and ownership fields end to end

For a multi-tenant application, a policy is only as reliable as its tenant context.

Check:

  • where tenant_id, organization_id, or the ownership field comes from;
  • whether the user can set or change that field;
  • whether inserts derive tenant context server-side or validate it with WITH CHECK;
  • whether updates can move a row into another tenant;
  • whether membership removal takes effect with the intended timing;
  • whether invitations, role changes, exports, and admin actions use the same boundary;
  • whether joins or helper functions accidentally broaden access.

Do not review policies in isolation from the application workflow. A syntactically correct condition can enforce the wrong business rule.

6. Use trusted authorization claims

Supabase warns that raw_user_meta_data is user-editable and should not hold authorization data. raw_app_meta_data is not directly editable by the user and is the more appropriate of the two for authorization context.

Even trusted app metadata can be stale until the user’s JWT refreshes. If removing a user from an organization must take effect immediately, design and test that revocation behavior rather than assuming every existing token contains fresh membership.

Also distinguish:

  • the anon Postgres role, used for unauthenticated requests; and
  • an anonymous Supabase Auth user, who has a user identity and uses the authenticated Postgres role.

Policies that confuse those two cases can grant or deny the wrong behavior.

7. Review views and functions separately

RLS attaches to tables, not every callable database object.

  • Supabase notes that views are created with security-definer behavior by default and may bypass underlying RLS. On Postgres 15 and later, security_invoker = true can make a view obey the caller’s RLS policies. Otherwise, revoke client access or keep the view in an unexposed schema.
  • RLS does not apply to functions. Grant EXECUTE only to roles that need it.
  • Review every SECURITY DEFINER function for its owner, fixed search_path, input validation, tenant checks, and exposed-schema placement.
  • Keep privileged helper functions out of exposed schemas unless direct client invocation is deliberate and controlled.

Treat a function that safely performs one narrow server-side action differently from a general-purpose RLS bypass.

8. Test an authorization matrix

Source review should be followed by bounded tests against a project and domain the reviewer is authorized to assess.

For a two-tenant app, a useful minimum includes:

  • unauthenticated request;
  • user A in tenant A;
  • a second user in tenant A with a different role;
  • user B in tenant B;
  • an invited or suspended user where relevant;
  • an approved admin role.

For each high-risk object and workflow, test:

  • allowed same-tenant read, create, update, and delete behavior;
  • denied cross-tenant access using identifiers the test owner created;
  • attempts to change ownership, tenant, price, role, approval, or status fields;
  • membership removal and token-refresh behavior;
  • negative cases for missing, expired, and lower-privilege sessions.

Use the user’s normal access token and publishable key for user-scoped tests. Do not initialize the test client with a secret or service_role key: those credentials bypass RLS and can make a broken test appear to pass.

Keep test data synthetic, requests bounded, and evidence redacted. “Returned no rows” and “request was denied” are different behaviors; record the actual status, response, role, and policy path.

9. Retest the fix

A complete finding should include:

  • result state, severity, and confidence as separate fields;
  • the affected object and operation;
  • the role and tenant context;
  • the policy or grant evidence;
  • the authorized request and response evidence when tested;
  • the business impact;
  • the smallest practical remediation;
  • retest status and evidence.

Do not mark an untested policy as passed. If a fix changed only SELECT, keep INSERT, UPDATE, and DELETE in Not tested until their behavior is checked.

Common failure patterns

  • A table created in a SQL migration never had RLS enabled.
  • SELECT is tenant-scoped, but INSERT accepts a caller-supplied tenant ID.
  • UPDATE checks the old row but permits changing its ownership in the new row.
  • A role claim comes from user-editable metadata.
  • A view or security-definer function bypasses the policy that appears to protect its base table.
  • A test uses the service key and therefore never exercises RLS.
  • The UI hides an admin action, but the server or policy accepts the same operation from a normal user.
  • A removed member retains access until a stale JWT expires, contrary to the product’s expected revocation behavior.

What the free scan can tell you

A public-surface scan may identify Supabase configuration and correctly classify a publishable key as public. It cannot query a table, inspect private migrations, enumerate policies, or test roles. A visible publishable key makes the RLS question more relevant; it does not answer it.

What requires a Human security review

An owner-run checklist works well for simple ownership rules. A Human security review becomes valuable when the policies encode tenant membership, invitations, delegated administration, approvals, billing entitlements, recovery, exports, or other workflows where the intended rule lives across database and application code.

The sample Human security review shows how cross-tenant evidence, impact, remediation, and retest status are separated.

Limitations

This checklist focuses on the Supabase Data API and Postgres RLS. Authentication configuration, Storage, Realtime, Edge Functions, network restrictions, backups, and application-server authorization may require separate review. The example catalog queries are read-only starting points and must be adapted to the project’s actual exposed schemas.

Revision history

  • 2026-07-27: Initial draft based on Supabase’s current RLS and Data API security documentation.