You opened the Supabase dashboard, saw a green RLS Enabled badge on every table, and felt fine. Then someone showed you that the SELECT policy body reads USING (true). This page is for the developer who needs to decide, in the next hour, whether the table is actually safe or whether the policy may as well not be there.
Short answer
A Postgres policy with the body USING (true) evaluates the boolean true for every visitor and lets every row through, while still counting as a policy in the pg_policies catalog. Per the Postgres CREATE POLICY documentation, permissive policies combine with the Boolean OR operator, so one USING (true) policy effectively cancels out every stricter policy on the same table. For user-owned rows, the correct body is auth.uid() IS NOT NULL AND auth.uid() = user_id.
What you should know
USING (true)on a SELECT policy is functionally identical to no policy for read access. The literal boolean true matches every row.- "RLS enabled" in the dashboard means the feature is turned on, not that any policy filters anything. The two states are independent.
- Permissive policies combine with OR. A stricter policy beside a
USING (true)policy does not constrain access; the OR resolves to true on the open branch. - AI builders default to
USING (true)because their dev-time tests run without a JWT, and a stricter body would correctly return no rows. - The Supabase database advisor flags multiple overlapping permissive policies, but a single
USING (true)on a single table passes the advisor and Lovable's 2.0 security scan.
What does "permissive" mean in Postgres RLS, and why is USING (true) the default?
Postgres has two policy types: PERMISSIVE, the default when you write CREATE POLICY without modifiers, and RESTRICTIVE. Per the Postgres CREATE POLICY documentation, "all permissive policies which are applicable to a given query will be combined together using the Boolean OR operator." A single permissive policy with the body true returns every row in the table, because the OR resolves to true on the first branch.
Supabase wraps this model and exposes policy creation through the dashboard. According to Supabase's official documentation on Row Level Security, auth.uid() returns null for unauthenticated requests, and the expression null = user_id evaluates to null, which Postgres treats as a denial. That is why the documentation recommends the explicit defensive form USING (auth.uid() IS NOT NULL AND auth.uid() = user_id) rather than the shorter USING (auth.uid() = user_id). Both work for authenticated traffic; the explicit form survives later edits unambiguously.
Three reasons the AI agent reaches for USING (true) on SELECT.
First, the agent's tests pass. When Cursor, Bolt, Lovable, v0, or Replit Agent scaffolds a feature, the test that follows runs without a JWT. A real policy that depends on auth.uid() correctly returns no rows. A USING (true) policy returns every row, and the feature passes its end-to-end check. The agent picks the body that makes the test green.
Second, the training data leans toward the simplest 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. The dev.to writeup that surveyed AI-generated RLS bodies across Cursor, Bolt, v0, and Lovable reports that the most common AI-generated SELECT body in audited projects is the bare USING (true).
Third, dashboard tooling rewards the green check. The Supabase RLS toggle is binary. As soon as a table has any policy attached, the dashboard reports it as protected, and Lovable's security scan agrees. Per Supabase's published AI prompt for generating RLS policies, the recommended template explicitly avoids USING (true) for user-owned tables, but the prompt is opt-in. Agents that do not load that prompt fall back to the simpler body.
What does a correct SELECT policy body look like?
The correct body depends on what the table holds and who is allowed to read what. Three patterns cover most schemas an AI agent will scaffold.
| Access shape | USING clause | WITH CHECK clause | Notes |
|---|---|---|---|
| User-owned rows (todos, files, notes) | auth.uid() IS NOT NULL AND auth.uid() = user_id | auth.uid() = user_id | The IS NOT NULL guard makes the policy fail closed for unauthenticated traffic. |
| Multi-tenant rows (org-scoped data) | org_id IN (SELECT org_id FROM memberships WHERE user_id = auth.uid()) | Same expression | The memberships table needs its own policy or the join leaks who belongs where. |
| Public read, authenticated write | USING (true) for SELECT | auth.uid() IS NOT NULL for INSERT or UPDATE | Public read is intentional for blog posts, catalogs, public profiles. |
The trap is the third row. USING (true) is correct on a SELECT policy for a public catalog. The same body on a table that holds user data is the vulnerability. The check is whether the rows in the table should appear in a search engine result. If no, the policy belongs in row one or two.
For INSERT and UPDATE on user-owned tables, the WITH CHECK clause is required separately. The Postgres docs are explicit: "existing table rows are checked against the expression specified in USING, while new rows that would be created via INSERT or UPDATE are checked against the expression specified in WITH CHECK." Without WITH CHECK, a user can update their own rows but write someone else's user_id into a new row.
How do you find every USING (true) policy in your project?
Four checks, in order of confidence:
- Run this SQL 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 clause; with_check is the WITH CHECK clause. Any row returned is a permissive body, and you can decide per row whether the table is supposed to be public. - Open the Supabase dashboard, go to Authentication, then Policies. Sort by the column showing the USING expression and scan for any row reading true.
- From a logged-out browser session, copy the anon key from your bundle and run
fetch('https://<project>.supabase.co/rest/v1/<table>?select=*', { headers: { apikey: '<key>', Authorization: 'Bearer <key>' }})against each sensitive table. Tables that return rows are accessible to unauthenticated traffic. - Use the Supabase database advisor under Database, Advisors. The advisor flags overlapping permissive policies and tables with RLS disabled, although it does not catch a single
USING (true)on its own.
For mobile apps that compile a Supabase backend into the binary, PTKD.com (https://ptkd.com) parses the project URL and anon key out of an APK or IPA and tests each REST endpoint against an unauthenticated query, surfacing the tables where a permissive policy is the only thing between an attacker 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. Per the Postgres documentation, "all restrictive policies which are applicable to a given query will be combined together using the Boolean AND operator," and "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 stricter permissive policy alongside a USING (true) policy is dead weight, because the OR short-circuits on true. If you want layered enforcement, the second policy has to be declared AS RESTRICTIVE.
Second, the policy you see in the dashboard is not always the policy in your source. AI builders sometimes regenerate the migration that created the table, dropping and recreating it without carrying forward the policies you wrote by hand. Move the corrected policy into the schema migration file so the next scaffold includes it in source.
Third, auth.role() = 'authenticated' is not the same check as auth.uid() IS NOT NULL AND auth.uid() = user_id. Some templates check the JWT audience instead of the user id, which lets every logged-in user read every other user's rows. Audit the qual column for any policy that names role instead of uid.
Fourth, fixing the policy does not retroactively unbreach the data. If the Supabase logs show queries against the affected tables from unexpected IPs in the weeks 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 SELECT policy with
USING (true)is the body AI builders generate by default, and it is functionally identical to no policy at all for read access. - "RLS enabled" in the Supabase dashboard says nothing about what the policy body contains. Audit qual and with_check directly in
pg_policies. - Permissive policies combine with OR, so a single
USING (true)cancels out every stricter policy beside it. UseAS RESTRICTIVEonly when you genuinely need an extra layer on top of a permissive base. - For mobile builds that ship a Supabase backend, PTKD.com (https://ptkd.com) scans the compiled APK or IPA and flags endpoints that respond to unauthenticated queries, which is one way teams catch the issue before submission to the App Store or Google Play.
- Move the corrected policy into your migration source so the next AI scaffold cannot drop and recreate the table without it.




