playbooks / database
Lock the Database Door
Row level security on every table, tested from the outside, so your anon key can shrug off strangers.

duck@duckaudit
Your anon key is in the bundle. That was always the plan. The policies were the other half of the plan.
how it burns you
Supabase hands every visitor your anon key on purpose; row level security is the thing that makes that safe. Skip it and your database is a public API with your users' data as the response body. Apps built fast with AI tools ship this exact miss constantly, and strangers check for it as a hobby.
Accept what the anon key is
The anon key is public by design and shipping it to the browser is correct. It only stays harmless while every table enforces row level security. No policies means the key is a skeleton key.
Turn on RLS for every table
Enable it everywhere, including tables you think nobody knows about. A table without RLS is readable and writable by anyone holding the anon key, which is everyone who ever opened your site.
alter table public.notes enable row level security;
alter table public.profiles enable row level security;
-- find stragglers:
select tablename from pg_tables
where schemaname = 'public'
and tablename not in (
select tablename from pg_tables t
join pg_class c on c.relname = t.tablename
where c.relrowsecurity
);Write policies that say who owns what
A policy is one sentence in SQL: this user can see rows where they are the owner. Write the four verbs separately so an update policy never accidentally grants deletes.
create policy "read own notes" on public.notes
for select using (auth.uid() = user_id);
create policy "insert own notes" on public.notes
for insert with check (auth.uid() = user_id);
create policy "update own notes" on public.notes
for update using (auth.uid() = user_id);
create policy "delete own notes" on public.notes
for delete using (auth.uid() = user_id);Keep the service key off the client, always
The service role key ignores every policy you just wrote. It belongs in server code only: route handlers, server actions, cron jobs. If it ever shipped to a browser, rotate it today.
Test the lock from outside
Policies you have not tested are wishes. Query as an anonymous stranger and as user A trying to read user B. Both should come back empty.
const anon = createClient(URL, ANON_KEY);
const { data, error } = await anon.from("notes").select("*");
// want: data is [] or error, never someone's notes
// and in the dashboard: run the Security Advisor, it flags missing policiesMake your AI do it
Paste this into Cursor or Claude Code from your project root.
Review my Supabase security. 1) From my migrations and schema, list every table in the public schema and whether row level security is enabled. 2) For each table, show existing policies and identify tables that have RLS enabled but no policy, or policies missing one of the four verbs. 3) Search the codebase for any use of the service role key and flag any path where it could reach client code. 4) Generate the missing alter table and create policy statements based on the obvious ownership columns, and a test script that queries each table with the anon key and asserts nothing private returns.
The 10 minute check
0/5receipts · every claim has a source
next playbook
The Server Believes No One →