The answer. Studio's table editor enables row-level security by default; raw SQL does not, and a table with RLS off is readable by anyone holding the publishable key. So the first migration for any table is three statements: create it, enable RLS, write the policy that opens exactly the rows the caller may see. A policy is a boolean expression evaluated per row as the calling role; auth.uid() is the caller's user id from the JWT (null for anonymous callers and in the SQL editor). Two things surprise everyone on day one: a table with RLS on and no matching policy returns an empty array with no error, and an INSERT policy does not let you read the row you just wrote, so the client's .insert().select().single() needs a SELECT policy as well.
The pattern.
create table public.notes (
id bigint generated always as identity primary key,
owner uuid not null default auth.uid() references auth.users (id) on delete cascade,
body text not null check (length(body) > 0),
created_at timestamptz not null default now()
);
alter table public.notes enable row level security;
alter table public.notes force row level security;
create policy "owner reads" on public.notes for select to authenticated using ((select auth.uid()) = owner);
create policy "owner writes" on public.notes for insert to authenticated with check ((select auth.uid()) = owner);
const { data, error } = await supabase.from("notes").insert({ body: "hello" }).select().single(); // needs both policies
Watch out.
to authenticatedis not decoration: it stops Postgres from evaluating the policy for roles it can never match, and it keeps anonymous callers out even when the expression would fail anyway.- Write
(select auth.uid()), notauth.uid(); the sub-select is evaluated once per statement instead of once per row. - Test as a user, not as
postgres: the SQL editor runs as the table owner, whereauth.uid()is null and, withoutforce, RLS is skipped entirely.
Related: rls-test-set-local-role-and-jwt-claims · update-policies-have-two-halves-using-and-with-check · rls-performance-wrap-auth-calls-index-the-policy-column