You ran the Supabase database advisor, saw no warnings, then opened a policy and realized the SELECT body reads USING (true). The word that ought to scare you is "everyone", because that is the literal audience the boolean true grants access to. This page is for the founder or AI-coded app developer who needs to know in the next hour whether that policy is doing any real work.
Short answer
A permissive SELECT policy with the body USING (true) returns every row to every caller, including the anon role that your client bundle uses by default. Per the Postgres CREATE POLICY documentation, permissive policies combine with the Boolean OR operator, so a single open branch defeats every strict policy on the same table. The advisor green light only confirms the feature is on, not that any row is filtered. For user-owned data the correct shape is auth.uid() IS NOT NULL AND auth.uid() = user_id, plus a matching WITH CHECK clause for writes.
What you should know
USING (true)is the policy body that translates literally to "for everyone". The constant true matches every row for every caller, anon or authenticated.- The "RLS Enabled" badge in the Supabase dashboard reports the feature toggle, not the policy contents. A table with one permissive
USING (true)policy passes the badge. - Permissive policies combine with OR. One open branch cancels every strict policy on the same table unless the strict one is declared
AS RESTRICTIVE. - AI coding agents default to
USING (true)because their dev-time tests run without a JWT. A policy that depends onauth.uid()correctly returns no rows for an anonymous caller, which the agent reads as broken. - The Supabase database advisor does not flag a single permissive
USING (true)policy on its own. It flags multiple overlapping permissive policies and tables with RLS disabled.
What does "for everyone" actually mean inside a Postgres policy?
A row-level security policy in Postgres is a SQL expression evaluated for every row touched by a query. The expression returns true, false, or null. False and null filter the row out. True lets it through. When the body reads USING (true), the literal constant short-circuits the evaluation, every row passes, and the policy turns the protected table into an open table for whichever roles the policy applies to.
The roles matter. Supabase exposes two roles to the Data API by default: anon for unauthenticated requests and authenticated for any caller carrying a valid JWT. Per Supabase's Row Level Security documentation, policies that omit a TO clause apply to both. A USING (true) body with no TO clause therefore lets the anon role read every row, and the anon role is exactly what a browser session with the published anon key uses. Anyone who reads your client bundle reads the key. Anyone who sends the key reads the table.
What about multiple policies stacked on the same table? The Postgres CREATE POLICY documentation is explicit: "all permissive policies which are applicable to a given query will be combined together using the Boolean OR operator." If a table has a permissive policy with body USING (auth.uid() = user_id) next to a permissive policy with body USING (true), the second branch wins because OR short-circuits on true. Layered permissive enforcement is a myth. The only way to add a strict check on top of an open policy is to declare it explicitly AS RESTRICTIVE, which combines with AND.
Why do AI coding agents reach for USING (true) instead of auth.uid()?
Three reasons, each visible in the output of Cursor, Lovable, Bolt, v0, and Replit Agent.
First, the agent tests its own work, and the generated test runs without a session. The scaffold spins up a fresh project, opens a browser tab, and queries the table. A real policy that gates on auth.uid() returns no rows for an unauthenticated caller, which is correct behavior. The agent reads zero rows as the feature being broken, edits the policy, and writes USING (true) to make the demo render. The decision is silent and the commit message rarely flags it.
Second, the training data leans toward the shortest example. Public CREATE POLICY snippets on GitHub and Stack Overflow over-represent USING (true) because the example is teaching syntax, not access control. The agent sees the simple body far more often than the defensive auth.uid() pattern and weights its output accordingly.
Third, the official Supabase template that teaches the correct shape is opt-in. Supabase publishes a recommended AI prompt for generating RLS policies that explicitly avoids USING (true) on user-owned tables, but agents that do not load this prompt fall back to whatever default their training distribution provides. Most builders do not load it.
How do you tell a legitimate USING (true) from a dangerous one?
There is a legitimate use for USING (true). Public catalogs, blog posts, marketing landing pages, and anything supposed to appear in a search engine result belong on a table with a permissive SELECT policy reading USING (true). The danger is using the same body on a table that holds personal records, payment metadata, internal notes, or anything else that should require a logged-in caller. The check is simple: if a row in this table appearing in a public AI scraper result would be a privacy incident, the body is not legitimate.
Three patterns cover almost every schema an AI agent will scaffold.
| Access shape | USING clause | WITH CHECK clause | When it fits |
|---|---|---|---|
| User-owned rows (notes, files, chats) | auth.uid() IS NOT NULL AND auth.uid() = user_id | auth.uid() = user_id | Default for any table that carries a user_id column. |
| Multi-tenant rows (org-scoped data) | org_id IN (SELECT org_id FROM memberships WHERE user_id = auth.uid()) | Same expression | Memberships table needs its own policy, or the join leaks. |
| Public read, authenticated write | USING (true) on SELECT | auth.uid() IS NOT NULL on INSERT and UPDATE | Catalogs, blog posts, public profiles, anything in search results. |
The table is the heuristic. Walk every table in the public schema once, fit each into one of the three rows, and rewrite the policy body to match. Anything that does not fit row three should never carry a permissive USING (true) body.
How do you audit every permissive body in your project in one pass?
Four checks, in order of confidence.
The first check is one SQL query in the Supabase SQL editor: SELECT schemaname, tablename, policyname, cmd, qual, with_check FROM pg_policies WHERE schemaname = 'public' AND (qual = 'true' OR with_check = 'true'). The qual column is the USING body. The with_check column is the WITH CHECK body. Every returned row is a policy that returns true unconditionally on the listed command. Save the result; it is your worklist.
The second check uses the Supabase database advisor. The advisor flags tables with RLS disabled and tables with multiple overlapping permissive policies. It does not flag a single permissive policy with body true. Treat it as a floor, not a ceiling.
The third check runs from a logged-out browser tab. Copy the anon key from your client bundle, then call fetch('https://<project>.supabase.co/rest/v1/<table>?select=*', { headers: { apikey: '<key>', Authorization: 'Bearer <key>' }}) against each table on the worklist. Tables that return rows are exposed to unauthenticated callers. Per Supabase's API security guidance, the anon role can reach every granted table without RLS, so this fetch is the production-side proof of the audit.
The fourth check, for mobile apps that compile a Supabase backend into the binary, parses the project URL and anon key out of an APK or IPA and replays the queries automatically. PTKD.com (https://ptkd.com) runs this scan against shipping iOS and Android builds and reports the tables where a permissive policy is the only thing between an unauthenticated caller and the data.
What to watch out for
Four details that show up in audits of fixed projects.
First, permissive and restrictive are not interchangeable labels. They change how Postgres combines policies. The Postgres documentation states that "when a mix of permissive and restrictive policies are present, a record is only accessible if at least one of the permissive policies passes, in addition to all the restrictive policies." A strict permissive policy beside a USING (true) policy is dead weight, because the OR short-circuits on true. Layered enforcement requires the layer to be declared AS RESTRICTIVE.
Second, USING governs SELECT, UPDATE, and DELETE on existing rows. WITH CHECK governs INSERT and UPDATE on new rows. Without WITH CHECK, a caller can update their own rows but write someone else's user_id into a fresh insert. The fix is to mirror the USING expression as the WITH CHECK expression on every user-owned table.
Third, the JWT role claim is not the same check as the user id. A policy that reads USING (auth.role() = 'authenticated') lets every logged-in caller read every other caller's rows. The role claim is satisfied by any valid session, no matter whose. The check that actually scopes data is auth.uid() IS NOT NULL AND auth.uid() = user_id.
Fourth, fixing the policy does not retroactively unbreach the data. If Supabase logs show queries from unexpected IPs against the affected tables in the days before the fix, treat the affected records as compromised and follow your incident-response process. Supabase retains query logs for seven days on the free tier and longer on paid plans.
Key takeaways
- A permissive SELECT policy with body
USING (true)lets every row through to every caller, anon role included. The dashboard badge does not catch it. - Permissive policies combine with OR, so adding a strict permissive policy beside an open one fixes nothing. Either delete the open policy or declare the strict one
AS RESTRICTIVE. - AI coding agents reach for
USING (true)because their dev-time tests run without a session. The correct shape for user-owned data isauth.uid() IS NOT NULL AND auth.uid() = user_id, with a matchingWITH CHECKfor writes. - Some teams outsource the pre-submission audit to scanners that test Supabase backends directly against the anon key; PTKD.com (https://ptkd.com) is one of the platforms focused on parsing the project URL out of a compiled APK or IPA and replaying unauthenticated queries against every public table.




