The kit ships RLS enabled with zero policies and routes all writes through the service role, so there is no write policy to get wrong, until you add one.
Last reviewed June 13, 2026 by SecureStartKit Team
The short answer
In Postgres Row Level Security, USING filters which existing rows a policy can read or target, while WITH CHECK validates the new row produced by an insert or update. A policy that sets USING but omits WITH CHECK lets a user write rows they could never read back, for example insert or update a row owned by another user. For any policy that allows writes, set WITH CHECK, usually to the same condition as USING, so the row a user writes must still belong to them.
Where it shows up: An INSERT, UPDATE, or FOR ALL policy defines USING but no WITH CHECK, so the resulting row is validated by nothing and a user can set a foreign owner id.
-- intends "users only touch their own rows", but only gates reads/targets
create policy "own notes"
on public.notes
for all
to authenticated
using (auth.uid() = user_id);
-- a normal user can now reassign a row they own to someone else:
-- update public.notes set user_id = '<other-user>' where id = '<their-row>';
-- USING passes (they own it now); no WITH CHECK validates the new user_idWith no WITH CHECK, an UPDATE is unconstrained on the resulting row. The owner can rewrite user_id to another account, because nothing validates the row after the change.
-- gate the target row AND the resulting row with the same condition
create policy "own notes"
on public.notes
for all
to authenticated
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
-- now the update above fails: the new user_id would not satisfy WITH CHECKAdding WITH CHECK (auth.uid() = user_id) means the row after an insert or update must still belong to the caller. Reassigning user_id to another account now violates the policy and is rejected.
-- USING is ignored on INSERT, so this constrains nothing on insert
create policy "insert own"
on public.notes
for insert
to authenticated
using (auth.uid() = user_id);INSERT evaluates only WITH CHECK. A FOR INSERT policy written with USING applies no constraint to the inserted row, so a user can insert rows with any user_id.
-- INSERT must be constrained with WITH CHECK
create policy "insert own"
on public.notes
for insert
to authenticated
with check (auth.uid() = user_id);WITH CHECK is the clause INSERT actually evaluates. Now an inserted row must have user_id equal to the caller, so a user can only create rows they own.
USING and WITH CHECK answer different questions. USING is applied to rows that already exist: which ones may this policy see or target. WITH CHECK is applied to the row that will exist after a write: is this new or modified row allowed. They are not interchangeable.
The command decides which one runs. SELECT and DELETE use only USING. INSERT uses only WITH CHECK, because there is no prior row to read. UPDATE uses both: USING gates which rows you may target, and WITH CHECK gates what they may look like afterward.
So consider a FOR ALL policy with only using (auth.uid() = user_id). On UPDATE, your update passes USING because you own the row now, but with no WITH CHECK there is no constraint on the new values, so you can change user_id to another account and hand the row away, or repoint it in a way the rest of your app trusts. On INSERT it is worse: USING is ignored entirely, so a policy that only sets USING places no constraint on inserts at all, and a user can insert rows with any user_id they like. The boundary holds when you read and leaks when you write, which is the most dangerous kind of half-protection because it looks closed in testing.
List your policies and find any write-capable one whose WITH CHECK is null. Postgres exposes this directly in pg_policies:
select schemaname, tablename, policyname, cmd, qual, with_check
from pg_policies
where with_check is null
and cmd in ('INSERT', 'UPDATE', 'ALL');
Every row this returns is a policy that allows writes without validating the resulting row. The qual column is the USING expression and with_check is the WITH CHECK expression; a write policy needs the latter.
Then test it as a normal user: try an UPDATE that changes the owner column to another id, and an INSERT that sets a foreign owner. If either succeeds, the policy is missing its WITH CHECK.
Myth“USING covers both reads and writes.”
USING gates existing rows for reads and for which rows a write may target. The new row produced by an insert or update is gated only by WITH CHECK. A write policy needs both.
Myth“If a user cannot read other rows, they cannot write them either.”
INSERT ignores USING entirely and evaluates only WITH CHECK, so a read restriction does not imply a write restriction. A user can insert rows they would never be allowed to read.
Myth“WITH CHECK is optional, so I can leave it off.”
It is only safe to omit for read-only or delete-only policies. For any policy that permits insert or update, omitting WITH CHECK removes the only validation of the resulting row.
Myth“RLS is enabled, so the table is protected.”
A table with RLS enabled and a write policy that lacks WITH CHECK is, for writes, weaker than it looks: the policy actively permits writes while validating nothing about the row being written.
SecureStartKit enables Row Level Security on every table and deliberately ships no policies, so the browser client can read and write nothing, and every write goes through the service-role admin client server-side after a `getUser()` check. That default sidesteps the WITH CHECK gap completely, because there are no client-facing write policies to misconfigure. This becomes yours to get right the moment you add a policy to let the client write a table directly. When you do, pair every write policy’s `USING` with a matching `WITH CHECK`, and verify with the pg_policies query that no insert, update, or all policy has a null WITH CHECK.
Row Level Security explained