All articles

Supabase RLS checker: review policies before launch

7 min read

Free security scan

Is your Next.js app secure? Find out in 30 seconds.

Paste your URL and get a full vulnerability report — exposed keys, missing headers, open databases. Free, non-invasive.

No code access requiredSafe to run on productionActionable report in 30

Use a supabase rls checker when you need a fast, structured answer to one launch question: can the public, authenticated users, or the wrong tenant read or change data they should not touch? The check should verify that RLS is enabled, policies match your app roles, writes are constrained, and anonymous access is intentional. For AI-built apps, this review is often the difference between a safe launch and a public database leak.

What the checker must verify?

A useful Supabase review is not just a scan for missing policies. It should model how your app actually reads and writes data from the browser, API routes, server actions, and background jobs. The main goal is to prove that each table has the smallest safe access path.

Start with these checks:

  • RLS enabled on every table containing user, payment, private, tenant, or operational data.
  • anonymous access allowed only for public content, marketing data, or intentionally readable resources.
  • tenant isolation enforced on every multi-tenant table, not just parent records.
  • write constraints tested for insert, update, delete, and upsert paths.
  • service role separation confirmed so privileged keys never reach the browser.

The highest-risk finding is usually a table that looks harmless but joins to sensitive data. For example, a public profiles table may expose user IDs that connect to orders, subscriptions, invite codes, or internal notes. A good Supabase RLS audit follows relationships, not just table names.

You should also verify that generated code did not create broad helper policies during prototyping. Common examples include using (true), with check (true), or policies that trust client-submitted user_id values. These may work during development, but they fail under hostile input.

If you are reviewing a broader launch surface, pair this database pass with a Supabase security scan. RLS is critical, but exposed storage buckets, leaked anon keys with permissive policies, and unsafe RPC functions often appear in the same release window.

Run a Supabase RLS checker

Run the check against staging or a production-like clone first. The database should contain realistic rows across at least two users, two organizations, and one admin-like role. Empty databases hide policy gaps because there is nothing meaningful to steal, overwrite, or enumerate.

A strong RLS policy review should test four access states: unauthenticated visitor, normal signed-in user, tenant admin, and backend service role. For each state, confirm which tables can be selected, inserted, updated, and deleted. The result should be explicit, not assumed from dashboard settings.

Look for no public tables unless the table is deliberately public. Marketing pages, public templates, and published posts may be acceptable. Customer records, billing data, waitlists, invite tables, AI prompts, embeddings, audit logs, and internal workflow tables should not be readable by anonymous users.

Use official guidance as a baseline, especially the Supabase RLS documentation. Then test your app-specific rules. Documentation explains the mechanism, but your risk comes from the actual policy expressions and how your frontend calls them.

The review should also inspect functions. Security-definer functions, RPC endpoints, and triggers can bypass expected paths if they are too permissive. A function that returns rows without checking auth.uid() or tenant membership can undo otherwise careful table policies.

For apps built quickly with AI tools, also run a security scan across the public app. It helps catch exposed routes, reachable admin pages, and frontend leaks that may combine with weak database rules.

Test policies with real roles

Policy names can look correct while the logic is wrong. The safest pattern is to test with real role assumptions and concrete rows. Create a small test matrix that proves what each actor can and cannot do.

For example, a user from organization A should not read, update, or infer records from organization B. A tenant admin may manage team members but should not update billing owner fields unless your app explicitly allows it. Anonymous users should fail closed unless a table is truly public.

Run SQL tests in staging, or use a controlled database branch. This short example shows the shape of a useful cross-tenant check:

sql
begin;
select set_config('request.jwt.claim.sub', '00000000-0000-0000-0000-000000000001', true);
select set_config('request.jwt.claim.role', 'authenticated', true);

select id, organization_id, owner_id
from projects
where organization_id <> 'expected-org-id';

insert into projects (name, organization_id, owner_id)
values ('cross-tenant test', 'other-org-id', auth.uid());
rollback;

The select should return zero rows. The insert should fail or be blocked by a with check policy that validates organization membership. If either operation succeeds, the policy is not enforcing the same boundary your product expects.

Pay close attention to select policy and using policy differences. In Postgres RLS, using controls visible or targetable rows, while with check controls whether new row values are acceptable. Many launch bugs happen when updates are restricted, but inserts still accept forged ownership fields.

Safer policies usually rely on auth.uid() ownership plus a membership lookup. For team apps, ownership alone is rarely enough. Use an organization membership table and restrict access through that relationship, not through client-controlled request fields.

A practical row level security check should also cover storage metadata if your app stores private files. File paths often include user IDs or organization IDs, and storage policies need the same isolation as tables. Public buckets are fine only when every object is intended to be public.

Fix risky RLS patterns

Most risky policies come from speed, not ignorance. During development, teams add broad rules to unblock the UI, then forget to tighten them before launch. AI-generated scaffolding can accelerate this because it optimizes for working features, not adversarial access.

Fix these patterns first:

  1. deny by default for new private tables, then add narrow policies only when a product path needs them.
  2. before production data, test every table with anonymous and authenticated requests.
  3. Replace using (true) with ownership, membership, publication status, or role-specific conditions.
  4. Remove client-trusted user_id and organization_id checks unless they are validated against auth.uid().
  5. Confirm service-role operations run only server-side, never through browser-exposed code.

A risky before-and-after pattern is simple. Before: any authenticated user can select all rows because the policy says using (auth.role() = 'authenticated'). After: the policy checks that the row belongs to a user or organization membership tied to the current session.

Also review generated API routes. A server route using the service role key can safely perform privileged work, but only if it validates the caller first. If a route accepts an arbitrary organization_id and queries with elevated privileges, RLS no longer protects you.

For a wider Supabase access review, use the Supabase security check alongside policy testing. That broader pass should include storage buckets, auth redirects, database functions, API keys, and exposed environment files.

If the app has sensitive business logic, consider a deep scan after you fix obvious issues. Deeper testing is useful when policies depend on nested team roles, paid plan limits, invitation flows, or admin dashboards.

Launch decision checklist

Before shipping, your decision should be evidence-based. Do not rely on “the app works” as a security signal. Working CRUD can still mean every signed-in user can enumerate every customer row.

Use this final checklist:

  • Every private table has RLS enabled.
  • Anonymous reads are documented and intentional.
  • Cross-tenant select tests return zero rows.
  • Inserts and updates cannot forge ownership fields.
  • Deletes are limited to the correct owner, role, or tenant.
  • Service role keys are server-only.
  • Storage policies match table-level privacy.
  • RPC functions validate the caller before returning data.

If those checks pass in staging with realistic data, you have a safe launch signal for the database layer. If any check fails, fix it before importing production data or inviting real users. RLS mistakes are quiet failures, and attackers usually notice them before customers report them.

Faq

Can automated checks prove every RLS policy is safe?

No. Automated checks can find missing RLS, broad public access, risky policy expressions, and obvious cross-tenant failures. They cannot fully prove your business rules are correct. Combine scanning with role-based test cases that reflect your actual app behavior, especially admin actions, billing ownership, and invitations.

Should i enable RLS on public tables?

Usually yes. Enabling RLS and adding an explicit public read policy is safer than leaving a table unprotected. It documents intent and reduces surprise if the table later gains sensitive columns. Public content should still avoid exposing internal IDs, private metadata, or unpublished records.

What is the most common Supabase RLS launch mistake?

The most common mistake is allowing all authenticated users to read or write rows because the app only tested happy-path ownership. A signed-in attacker is still an attacker. Test with another user, another organization, forged IDs, and direct API calls before launch.

If you want a fast prelaunch pass, AISHIPSAFE can scan your app surface and help identify security issues worth fixing before users arrive.

Free security scan

Is your Next.js app secure? Find out in 30 seconds.

Paste your URL and get a full vulnerability report — exposed keys, missing headers, open databases. Free, non-invasive.

No code access requiredSafe to run on productionActionable report in 30