With Supabase, your Postgres database is exposed to the client through a REST/SDK layer protected by the anon key. Anyone with that key — which means anyone using your app — can attempt to query any table. RLS policies are what decide which rows each request can actually see or change. When RLS is missing or written loosely, the database happily returns rows it shouldn't. The goal of an RLS audit is to confirm that every table holding user or tenant data enforces ownership at the row level.

How a Row Level Security policy filters rows for the signed-in userBrowserSupabaseanon keyRLS policyuser_id = auth.uid()Own rowsOther tenants blocked
Every browser query goes through the anon key and is filtered by your RLS policy before any row is returned.

Mistake 1: RLS is never enabled on a table

Enabling RLS is a per-table switch. Create a new table during a late-night feature sprint, forget the switch, and that table is readable and writable by anyone with the anon key. AI tools that scaffold tables are especially prone to skipping it.

  • List every table and confirm ALTER TABLE ... ENABLE ROW LEVEL SECURITY is set.
  • Pay special attention to join tables, audit logs, and anything added after the initial schema.
  • Remember: enabling RLS with no policies denies all access through the anon key — that's safe by default, then you add policies deliberately.

Mistake 2: Policies that use USING (true)

A policy of USING (true) technically satisfies "RLS is enabled" while granting access to every row. It is the most common false sense of security in Supabase apps. It usually appears when someone enabled RLS, hit an access error, and "fixed" it by allowing everything.

  • Replace USING (true) with a condition that references auth.uid(), auth.jwt(), or a membership lookup.
  • Scope reads to the owner: USING (user_id = auth.uid()) for per-user data, or a join against an organization-members table for team data.
  • Treat any USING (true) on a sensitive table as a launch blocker, not a warning.

Mistake 3: Missing WITH CHECK on insert and update

USING controls which rows a policy can see; WITH CHECK controls which rows a user is allowed to write. If you add a SELECT policy but forget WITH CHECK on INSERT/UPDATE, a user may be able to create or modify rows that belong to someone else — for example, inserting a record with another workspace's ID.

  1. Add WITH CHECK to every INSERT and UPDATE policy on tenant data.
  2. Make WITH CHECK enforce the same ownership condition as your read policy.
  3. Block users from changing the owner or tenant ID of an existing row through an UPDATE.

Audit every table's RLS policies automatically and see which ones leak.

Try the Supabase RLS checker

Mistake 4: The service-role key reaches the client

The service-role key bypasses RLS entirely — that's its purpose for trusted server work. If it is imported into client code, an edge function that returns it, or a public environment variable, RLS becomes irrelevant because the attacker can act as a superuser.

  • Confirm the service-role key only lives in server-side code that never ships to the browser.
  • Check that no API route echoes the key back in a response or error.
  • Use the anon key on the client and reserve the service role for narrow, audited server operations.

Mistake 5: Policies that don't actually check tenancy

In multi-tenant apps, ownership often runs through an organization or membership table. A policy can look strict while checking the wrong thing — for example, confirming the row has some organization rather than the caller's organization.

  • Make sure team-scoped policies join to a members table and compare against auth.uid().
  • Verify the policy fails for a user who belongs to a different organization, not just for anonymous users.
  • Watch for policies that reference a tenant ID taken from request data instead of the verified session.

Mistake 6: Supabase Storage buckets left public

RLS audits often stop at tables and forget Storage. Buckets have their own access rules, and a bucket set to public — or with permissive storage policies — can expose uploaded invoices, ID documents, or user files by URL.

  • Set user-content buckets to private and serve files through signed URLs or an authorized route.
  • Write storage policies that scope objects to the uploading user or workspace.
  • Validate file type and size on upload to reduce the risk of malicious or oversized files.

How to run the audit

A solid RLS audit is methodical. The most reliable check is behavioral: sign in as a second tenant and try to reach the first tenant's data through the client SDK and through each API route.

  1. Enumerate every table and bucket, and record whether RLS / storage policies are enabled.
  2. Read each policy and classify it: properly scoped, USING (true), or missing WITH CHECK.
  3. Trace where the service-role key is used and confirm it never reaches the client.
  4. Run cross-tenant tests with two accounts and confirm each one only sees its own rows and files.

This is exactly the kind of repeatable, pattern-based review that's easy to automate and easy to forget by hand. Catching a single missing policy before launch is far cheaper than discovering it after a user reports seeing someone else's data.

Get an RLS-focused report that separates real tenant-isolation bugs from routine cleanup.

Scan your Supabase project