Skip to content
StackSiren

What does USING (true) mean in a Supabase RLS policy?

By StackSiren · Published

using (true) means the policy's condition always passes for every role the policy targets. Every row those roles can already reach through grants is allowed. That is correct for genuinely public read-only data, and wrong for anything private.

People often add it after enabling RLS made the app return blank pages. The blank page was the safe state. An always-true policy is the open door. Supabase's RLS docs use using (true) on purpose for public feeds; Bolt's Security Audit and Supabase's advisors flag the same pattern as RLS Policy Always True when it looks too open.

What USING (true) actually does

A policy is an extra WHERE clause Postgres adds to every matching query. Compare:

-- Always true: every row the role is granted on
create policy "notes_all_read" on public.notes
  for select to anon, authenticated
  using (true);

-- Owner-scoped: only the caller's rows
create policy "notes_select_own" on public.notes
  for select to authenticated
  using ((select auth.uid()) = user_id);

Important details:

  • The to clause still matters. to authenticated using (true) does not mean "my users". It means anyone who can sign up. On an app with open registration, that is close to public.
  • using filters existing rows (select / update / delete). with check (true) on insert or update accepts any new row shape the grants allow — including rows owned by someone else if your columns permit it.
  • Grants still apply. using (true) cannot invent a privilege the role doesn't have, but on many projects anon and authenticated already have broad grants on public tables.

When USING (true) is OK

It is appropriate for public, read-only data — for example published posts, a restaurant menu, or a marketing FAQ — when you also lock writes:

alter table public.announcements enable row level security;

revoke all on table public.announcements from anon, authenticated;
grant select on table public.announcements to anon, authenticated;

create policy "Anyone can read announcements"
  on public.announcements for select
  to anon, authenticated
  using (true);

That pattern matches Supabase's own public-feed example: select for everyone, no insert/update/delete grants for client roles. Prefer a real predicate when you have one (published = true, is_public = true) so draft rows stay hidden:

create policy "posts_public_read" on public.posts
  for select to anon, authenticated
  using (published = true);

When it is not OK

Never leave using (true) (or with check (true)) on:

  • Per-user notes, orders, messages, profiles, or settings
  • Anything with PII, payment state, or API tokens
  • Write policies (insert / update / delete) unless you truly intend a public writable board — and even then, think hard about spam and abuse

A close cousin that looks safer but isn't: using (auth.uid() is not null). That only proves someone is signed in. Any registered user still sees every row.

Safer rewrites

Owner-only table (replace an always-true catch-all):

drop policy if exists "notes_all_access" on public.notes;

create policy "notes_select_own" on public.notes
  for select to authenticated
  using ((select auth.uid()) = user_id);

create policy "notes_insert_own" on public.notes
  for insert to authenticated
  with check ((select auth.uid()) = user_id);

create policy "notes_update_own" on public.notes
  for update to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);

create policy "notes_delete_own" on public.notes
  for delete to authenticated
  using ((select auth.uid()) = user_id);

Public read, owner write:

drop policy if exists "posts_open" on public.posts;

create policy "posts_public_read" on public.posts
  for select to anon, authenticated
  using (published = true);

create policy "posts_owner_write" on public.posts
  for all to authenticated
  using ((select auth.uid()) = author_id)
  with check ((select auth.uid()) = author_id);

Full walkthroughs with curl tests: Lovable Supabase RLS and Bolt + Supabase RLS checklist. If a secret key was used to "get past" RLS, rotate it: service_role key exposed.

Finding always-true policies with pg_policies

Run this in the SQL editor on your project:

select schemaname, tablename, policyname, roles, cmd, qual, with_check
from pg_policies
where schemaname = 'public'
  and (
    qual is null
    or qual in ('true', '(true)')
    or with_check in ('true', '(true)')
    or qual ilike '%true%'
    or with_check ilike '%true%'
  )
order by tablename, policyname;

qual is the using expression; with_check is the write check. Review every hit by hand — a policy whose expression contains the word true might be fine (published = true), while a bare true on a private table is not.

Also open Supabase Database → Linter / Security Advisor, or Bolt's database Security tab if you use Bolt Cloud / a connected project. Both surface "always true" style findings. Dismiss a warning only when you have intentionally made that table public and removed write grants.

Prove the rewrite

# Signed-out: private tables must not return rows
curl -s "https://YOUR-PROJECT.supabase.co/rest/v1/notes?select=*&limit=5" \
  -H "apikey: YOUR_PUBLISHABLE_KEY"

# Public feed: empty or published rows only — never drafts
curl -s "https://YOUR-PROJECT.supabase.co/rest/v1/posts?select=id,title,published&limit=5" \
  -H "apikey: YOUR_PUBLISHABLE_KEY"

Then repeat with two signed-in users as in the Lovable and Bolt guides. Our Supabase RLS checker covers the signed-out case on sites you own. More detail: the Supabase RLS guide.

Common ways this shows up in AI-built apps

  1. Blank page after enabling RLS. Someone turns RLS on, the app returns no rows, and the quickest "fix" is a policy with using (true) for all operations. The UI works again; so does every stranger with your publishable key.
  2. No sign-in yet. Bolt's security docs note that without sign-in, builders often write policies that let any visitor read and write. The Security Audit then flags RLS Policy Always True. That warning is only acceptable if you truly want a public writable table.
  3. for all with using (true). One broad policy covers select, insert, update and delete. Prefer one policy per command so you can keep public read while locking writes.
  4. Public Storage mistaken for public tables. A public Storage bucket serves files by URL; that is separate from table RLS. Don't open a files metadata table with using (true) just because the bucket is public — scope the metadata to owners and use signed URLs for private objects.
  5. Trusting the linter dismiss. Advisors flag always-true rules because they are a common foot-gun. Dismiss only after you have select-only grants and a clear reason the data is public.

How builders and advisors surface it

  • Supabase Database linter / Security Advisor lists permissive policies. Open each finding and read the SQL before you ignore it.
  • Bolt database Security (Security Audit) uses the same family of checks and shows RLS Policy Always True with Ask Bolt to fix. Review the SQL Bolt proposes; ask specifically for auth.uid() = user_id and with check on writes.
  • Lovable security scans similarly flag access rules that let everyone through. Still run the curl tests — automated checks won't know your product rules.

None of these tools replace a signed-out and second-user test against your own project.

Step-by-step: replace one bad policy

Suppose pg_policies shows this on a private orders table:

-- Bad: any role the policy targets sees and changes every order
create policy "orders_open" on public.orders
  for all to authenticated
  using (true)
  with check (true);

Replace it like this:

begin;

drop policy if exists "orders_open" on public.orders;

create policy "orders_select_own" on public.orders
  for select to authenticated
  using ((select auth.uid()) = user_id);

create policy "orders_insert_own" on public.orders
  for insert to authenticated
  with check ((select auth.uid()) = user_id);

create policy "orders_update_own" on public.orders
  for update to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);

create policy "orders_delete_own" on public.orders
  for delete to authenticated
  using ((select auth.uid()) = user_id);

-- Optional but recommended on older projects with broad defaults
revoke all on table public.orders from anon;
grant select, insert, update, delete on table public.orders to authenticated;

commit;

Then run the signed-out curl (should not return orders) and the two-user test. If the app breaks, fix the client to send the signed-in session — don't reopen the policy.

FAQ

Why did my AI builder add USING (true)? Often to make the UI work again after RLS was enabled with no policies. Ask it for owner-scoped policies with with check instead, then re-test.

Is USING (true) on authenticated only safer? Only a little. Anyone who registers becomes authenticated. For private data, compare auth.uid() to an owner column.

Does dropping the policy lock the table? With RLS enabled and no matching policy, client roles get zero rows (or a grant error). That is the safe default until you add a scoped policy.

What about WITH CHECK (true)? Same idea for writes: every insert/update the grants allow is accepted. Prefer with check ((select auth.uid()) = user_id) on owner tables.

How do I keep this from coming back? Re-run the pg_policies query after every schema change, and scan the live app after deploys. StackSiren's first scan is free; it looks at what your public API returns to a signed-out visitor on sites you own.

Is using (published = true) the same as using (true)? No. A real column predicate still filters rows. Advisors may still mention policies that look broad; judge them by whether unpublished or private rows can leak.

Can I use using (true) for insert on a contact form? Sometimes. A public insert-only table (contact messages) can use with check (true) for anon insert if you revoke select/update/delete from anon so visitors cannot read or wipe other people's submissions. Prefer rate limiting and spam controls on top. Never combine public insert with public select on the same sensitive table.

Check your app now

Free passive scan, results in under a minute.

Run a free scan

Fix guides for this topic

Platform checklists: Lovable · Bolt

Related posts

Sources