Vibemast checklist: Supabase RLS checklist for AI-built apps

Supabase RLS checklist for AI-built apps

Supabase is a common choice for apps built with AI agents, and Row Level Security (RLS) is the part agents most often get wrong or skip. The reason is simple: an app works fine with RLS off, so nothing fails in testing. Then you launch, and anyone with your public key can read the table. This checklist is for checking an AI-built Supabase app before you ship. Each item is something you can verify yourself, and each is based on Supabase's own documentation.

Checked on 27 Oct 2026 against Supabase's Row Level Security guide and server-side auth docs. This is a practical checklist, not a full security audit.

Why this matters

Supabase exposes your database through an API. The key in your front-end code is meant to be public, so what protects your data is the database itself. Supabase's docs say that a table in an exposed schema without RLS can be read and written by any role that has been granted access. With RLS on and no policies, the API returns no data to the public key, which is safe but unhelpful. The risky state is the first one, and AI-generated migrations sometimes leave tables in it.

The checklist

1. List every table and see which ones have RLS off

select schemaname, tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;

Run this in the SQL editor. Any row with rowsecurity = false is open to anyone who holds a grant on it. Check other schemas you have exposed too. The dashboard's security advisor also flags tables without RLS.

2. Turn RLS on for every table in the same migration that creates it

Do not rely on remembering later. Put alter table ... enable row level security next to each create table, and tell your agent to do the same.

3. Write one policy per operation and name the role

Supabase recommends separate policies for select, insert, update and delete, each with a to clause. A typical "users own their rows" table looks like this:

create table public.notes (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null default auth.uid()
    references auth.users (id) on delete cascade,
  body text not null
);

alter table public.notes enable row level security;

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);

create index notes_user_id_idx on public.notes (user_id);

Three details here come from the docs. Wrapping auth.uid() in select lets Postgres evaluate it once instead of per row. An update policy needs both using (which rows can be touched) and with check (what they may become), and it also needs a matching select policy. Indexing the column your policies filter on keeps queries fast.

4. Look for policies that allow everything

Search your migrations for using (true) and with check (true). These are fine for genuinely public data, such as a list of published posts, and only for select. On insert, update or delete they usually mean the agent gave up on writing a real rule.

5. Never authorise from user_metadata

Users can edit their own user_metadata. A policy that says "role is admin in user_metadata" can be satisfied by anyone who changes their own profile. Supabase's docs say to use app_metadata for authorisation data, since users cannot change it.

6. Check views

Views run with their owner's permissions by default, so they bypass RLS on the tables underneath. On Postgres 15 and later, create them with security_invoker = true so they respect the caller's access. Otherwise keep views out of exposed schemas.

7. Check functions that use security definer

A security definer function runs with the creator's rights and can bypass RLS. If you need one, set search_path = '' and do not put it in an exposed schema, because anyone could call it through the API.

8. Keep the secret key on the server

The secret key (and the older service_role key) bypasses RLS completely. It belongs only in server-side code and environment variables with no NEXT_PUBLIC_ or VITE_ prefix. Search your built front-end files and your git history for it. If it has ever been exposed, rotate it.

9. Check storage buckets

Files in Supabase Storage follow their own policies. Check each bucket: is it public, and who can upload, overwrite or delete? A public bucket is readable by anyone with the URL.

10. Test with two real accounts

Policies that read correctly can still be wrong. Sign in as user A, create a row, then sign in as user B and try to read, edit and delete it through the same client your app uses. You can also test in SQL by switching role inside a transaction:

begin;
set local role authenticated;
select set_config('request.jwt.claims',
  '{"sub":"00000000-0000-0000-0000-000000000001","role":"authenticated"}', true);
select count(*) from public.notes;  -- should show only user 1's rows
rollback;

For repeatable checks, Supabase supports pgTAP tests through supabase test db.

Ask your agent to audit it, with a rule not to change anything

An agent can do the first pass of this checklist faster than you can, as long as it reports first and does not edit. This is the "Database access rules" prompt from the 20 security prompts in the Vibemast kit:

List every database table, bucket and storage location. For each, state who
can read, insert, update and delete it through the public client or API, and
what enforces that (row-level security policies, server checks, or nothing).
Flag every table with no rules, rules that allow everything, rules based on
user-editable fields, and views or functions that bypass the rules. Then write
a test (or SQL script) that proves user A cannot read or change user B's rows.
Report first. Do not change policies until I approve.

The final line matters. Agents are keen to "fix" things, and a policy change you have not reviewed can lock your own users out or open the table further.

Where to go next

RLS is one part of launch readiness. For the rest, including secrets, auth, rate limits, backups and error handling, read the pre-launch security checklist for vibe-coded apps. The Vibemast Ship-It Kit has the full set of 20 security audit prompts, a one-page checklist PDF, and a Next.js and Supabase rules file that tells your agent to write RLS in every migration. You can start with the free mini pack if you want to see the format first. None of this replaces a professional review if you hold sensitive data such as health or payment details.

Make sure the agent does not undo it

Fixing RLS once is not enough if the next agent session adds a table without a policy. Three things keep it fixed:

  1. Add a rule: "Every new table gets RLS enabled and policies in the same migration." The Cursor rules for Next.js and Supabase guide has a copy-ready version.
  2. Never let the service-role key reach the browser. Check every NEXT_PUBLIC_ and VITE_ variable.
  3. Re-run this checklist before every release, together with the Pre-launch security checklist for vibe-coded apps.

The Ship-It Kit includes the Next.js and Supabase rules with these RLS rules built in, and audit prompts for database access, authorisation and secrets sent to the browser.

Who the paid kit is for, and who it is not for

  • Worth it if you build on one of its four stacks (Next.js with Supabase, Vite with React, FastAPI, Phaser) and want tested files and prompts today instead of writing and testing your own.
  • Not worth it if you only need one rules file for one project. The free guides and the free mini pack are enough, and you can adapt them yourself.

Related guides

FAQ

Is it safe to expose the Supabase publishable (anon) key?

Yes, that is its purpose, as long as RLS is enabled with correct policies. The key identifies your project; the policies decide what it can do.

Do I need RLS if all my queries go through my own server?

If every request really goes through your server, and the browser never talks to Supabase directly, you have less exposure. But the Data API is still reachable by anyone who has your project URL and publishable key, so enable RLS anyway.

Why does my query return no rows after enabling RLS?

RLS with no policies denies access. Add a policy for the role and operation you need, and check that you are signed in, since auth.uid() is null for signed-out users.

Can the agent write RLS for me?

It can draft it, and it is good at the boilerplate. Review each policy yourself, and test with two accounts before launch.

Back to blog

Skip the setup: get the Ship-It Kit

Agent rules for 4 stacks, a generator for AGENTS.md, CLAUDE.md and Cursor rules, 25 build prompts, 8 rescue prompts and 20 security audit prompts.

Launch price US$17 Regular price US$22

Get the Ship-It Kit

Prices in US dollars. Your bank converts at checkout.Made in New Zealand by Vibemast.

Launch price for early buyers. Instant download. One-time payment. Not working? Fixed or refunded within 14 days.

For business owners

Run a business, not just code?

Our free Business Brain setup prompt makes ChatGPT, Claude or Gemini know your business, so replies, quotes and posts sound like you. It takes about 10 minutes and needs no technical skills. Handy for your own business, or for clients who keep asking how to use AI.