Supabase WITH CHECK (true): what that INSERT policy actually opens
By Pritesh Ghodge, security engineer · Last updated 6 November 2026
Already used with check (true) to silence an insert error?
The Abbi Labs quick check scans your live app for leaked keys in its JavaScript, Supabase tables anyone can read with your public key, exposed .env/.git files and security headers. No sign-up. For write policies like with check (true), use the free RLS policy checker.
Weekly AI-app security checklist
One short email a week: a practical check for apps built with Supabase, Next.js and AI tools (RLS, leaked keys, headers and more). Unsubscribe anytime.
You hit new row violates row-level security policy, asked an AI assistant to fix it, and the insert started working. Look at the policy it wrote. If the WITH CHECK side is just true, the error is gone because every new row now passes. On a table of user-owned rows, that is not a fix. It is an open write.
This guide is about that policy shape itself: what with check (true) opens, how to find every one of them, when it is actually fine, and the owner-scoped replacement. For the insert error and its other causes, see new row violates row-level security policy.
USING vs WITH CHECK (from the docs)
From the Postgres CREATE POLICY docs and Supabase's RLS guide:
- USING filters existing rows: what SELECT can see, and which rows UPDATE and DELETE can touch. Rows that fail are silently skipped (no error).
- WITH CHECK checks new rows: what INSERT can add, and what a row may look like after UPDATE. Rows that fail raise "new row violates row-level security policy".
- INSERT policies only have WITH CHECK. SELECT and DELETE only have USING. UPDATE and FOR ALL can have both. If WITH CHECK is missing on UPDATE or ALL, Postgres uses the USING expression for new rows too.
- With RLS on and no applicable policy, the answer is no. Permissive policies are ORed, so one loose
with check (true)opens writes even if every other policy is strict.
So using (true) and with check (true) are not the same hole. One opens reads (and the "which rows can I touch" side of updates/deletes). The other opens the shape of every new row.
What WITH CHECK (true) actually opens
Take a typical AI-builder "fix" on a todos table:
create policy "allow inserts"
on public.todos for insert
to authenticated
with check (true);Every signed-in user can now insert a row with any user_id, including another user's. The policy never looks at the row. It only asks "are you authenticated?"
That gets worse in two common ways:
- Open sign-ups. On most Supabase projects anyone can create an account. "Authenticated" then means any stranger who signed up thirty seconds ago. See the anon key guide for why the public key plus open sign-up is the real attack surface.
to publicorto anon. Then no account is needed. Anyone with your publishable/anon key (which ships in the browser by design) can insert any row.
The same pattern on UPDATE is just as bad. with check (true) (or an UPDATE that only has using (true) and no WITH CHECK, so USING is reused) lets the caller rewrite a row into any shape, including changing user_id to steal ownership.
When WITH CHECK (true) is fine
It is the right tool when the table really is a public inbox:
- A contact or waitlist form anyone may submit
- Anonymous feedback or a public "report a bug" table
- A newsletter signup that does not belong to a logged-in user
Even then, tighten the edges:
- Name the role:
to anonorto authenticated, not the default of every role. - Keep SELECT closed unless submissions are meant to be public. A public insert with a public select is a public dump of every message.
- Grant only INSERT (and only the columns you need), and put a rate limit or CAPTCHA in front of the form, because the public key lets anyone script it.
How to find every with_check = true
In the Supabase SQL editor:
select schemaname, tablename, policyname, roles, cmd, qual as using_expr, with_check
from pg_policies
where schemaname not in ('pg_catalog', 'information_schema')
and (
replace(replace(lower(coalesce(with_check, '')), ' ', ''), E'\n', '') in ('true', '(true)')
or (
cmd in ('UPDATE', 'ALL')
and with_check is null
and replace(replace(lower(coalesce(qual, '')), ' ', ''), E'\n', '') in ('true', '(true)')
)
)
order by schemaname, tablename, cmd;Every row is a write path that accepts any new values for the listed roles. Confirm each one is meant to be a public inbox. Anything with a user_id, owner_id, profile_id or similar column almost certainly is not.
For a full pass that also catches RLS off, using (true), FOR ALL, and policies that never mention auth.uid(), paste the output of the free RLS policy checker query instead.
The owner-scoped fix
Drop the open policy and replace it with one that ties the new row to the caller. From Supabase's own RLS examples:
drop policy "allow inserts" on public.todos;
create policy "users insert own todos"
on public.todos for insert
to authenticated
with check ((select auth.uid()) = user_id);
-- if the app doesn't send user_id, let the database fill it in:
alter table public.todos
alter column user_id set default auth.uid();Mirror the same idea on UPDATE so a user cannot reassign ownership:
create policy "users update own todos"
on public.todos for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);Wrapping auth.uid() in (select ...) is the pattern Supabase recommends so Postgres can cache the value per statement instead of calling it per row.
Prove it with two accounts
The SQL editor runs as postgres, which bypasses RLS, so "it works in SQL" proves nothing. Run an insert as a signed-in user inside a transaction that rolls back:
begin;
set local role authenticated;
set local request.jwt.claim.sub = 'USER-A-UUID';
-- should succeed:
insert into public.todos (title, user_id)
values ('mine', 'USER-A-UUID');
-- should fail:
insert into public.todos (title, user_id)
values ('stolen', 'USER-B-UUID');
rollback;Then do the product test: sign in as user B in the app and try to create or update a row that belongs to user A. If either succeeds, the policy is still open.
What Security Advisor catches (and skips)
Supabase Security Advisor lint 0024 (rls_policy_always_true) flags permissive policies whose condition is always true. In the current splinter SQL it flags:
- INSERT (and similar) policies with
WITH CHECK (true)/1=1-style checks - UPDATE, DELETE and ALL policies with always-true USING (and the matching open WITH CHECK cases)
It deliberately skips SELECT policies with USING (true), because public read access is often intentional. That gap matters: a table can look "Advisor-clean" for reads while still having an open insert. The SQL above, the policy checker, and the two-account write test cover what the lint leaves alone.
A prompt for Lovable, Bolt or Cursor
Audit my Supabase policies for with check (true) and using (true).
Don't disable RLS and don't use the service_role key in the frontend.
1. Run: select tablename, policyname, roles, cmd, qual, with_check
from pg_policies where with_check in ('true', '(true)')
or (cmd in ('UPDATE','ALL') and with_check is null and qual in ('true','(true)'));
2. For every user-owned table (has user_id / owner_id), replace those
policies with owner-scoped ones:
with check ((select auth.uid()) = user_id)
and the matching using ((select auth.uid()) = user_id) on update/delete/select.
3. Leave with check (true) only on tables that are meant to be public inboxes
(contact form, waitlist), and keep their SELECT closed.
Show me the SQL before applying it.What an outside scan can and can't see
From outside, a scanner can see your live JavaScript and whether a table is readable with your public key. It cannot see your policies and should not test writes against a live app. That is why with check (true) needs the policy checker or the SQL above, plus the two-account test. The Abbi Labs quick check covers headers, leaked keys in public JS, /.env and /.git, and limit=1 anon-key table reads. Write holes are the policy checker's job.
Real incidents where missing or permissive RLS on Lovable/Supabase apps leaked data are covered in the CVE-2025-48757 breakdown.
Keep it that way
Policies get loosened in the middle of a late-night "fix this error" session and nobody notices until the data is somewhere it shouldn't be. Abbi Labs scans your live app for leaked keys, Supabase tables readable with your public key, exposed .env/.git files and security headers. Monitor re-scans weekly and after every deploy, and emails you only when something new appears. For the write side, re-run the policy checker whenever you add a table or let an AI assistant "fix" an RLS error.
FAQ
What does WITH CHECK (true) mean on a Supabase policy?
WITH CHECK decides whether a new or updated row is allowed. true means every new row passes, so any role the policy targets can insert (or rewrite) a row with any values, including someone else's user_id. USING filters existing rows for SELECT, UPDATE and DELETE; WITH CHECK is what INSERT and the new-row side of UPDATE use.
Is WITH CHECK (true) ever OK?
Yes, when anyone really should be able to add any row, like a public contact form, waitlist or feedback table. Even then, name the role (to anon or to authenticated), keep SELECT closed unless the submissions are meant to be public, and consider a rate limit, because the public key lets anyone call it from a script.
What's the difference between USING (true) and WITH CHECK (true)?
USING (true) on SELECT lets that role read every existing row. WITH CHECK (true) on INSERT lets that role add any new row. An UPDATE policy can have both: USING picks which rows you can touch, WITH CHECK decides what the row may look like after the change. If WITH CHECK is missing on UPDATE or FOR ALL, Postgres reuses the USING expression for new rows too.
Why is "to authenticated" with WITH CHECK (true) still dangerous?
Because open sign-ups are the default on most Supabase projects. Anyone who creates an account becomes authenticated. A policy that only checks "is signed in" then lets any stranger insert rows for any user_id. Scope the row to (select auth.uid()) = user_id, not just to authenticated.
Does Supabase Security Advisor catch this?
Partly. Lint 0024 (rls_policy_always_true) flags WITH CHECK (true) on INSERT, and always-true USING on UPDATE, DELETE and ALL. It deliberately skips SELECT policies with USING (true), because public reads are often intentional. It also won't catch a policy that looks strict but compares the wrong column, so still run the SQL below and a two-account write test.
Does Abbi Labs check for WITH CHECK (true)?
Yes. The free RLS policy checker flags INSERT and UPDATE policies with WITH CHECK (true), UPDATE policies with USING (true), FOR ALL policies that are open, tables with RLS off, and policies on user-owned tables that never reference auth.uid(), from one read-only SQL query analysed in your browser. The no-sign-up quick check on the home page scans your live site from outside for leaked keys in JavaScript, Supabase tables readable with your public key (limit=1), exposed .env/.git files and security headers. It doesn't test writes.
Related guides
- Free tool: Supabase RLS policy checker
- Supabase "new row violates row-level security policy": fix it without opening your table
- How to check if your Supabase tables are readable by anyone
- Is your Supabase anon key safe to expose?
- CVE-2025-48757 breakdown (Lovable / Supabase RLS)
- Lovable app security checklist