VibeSecurity

Free tool, runs in your browser

Generate Supabase RLS policies you can test

Pick your table and who should have access. You get Row Level Security SQL written the way Supabase recommends, a plain-English summary of what each policy allows, and a rollback-safe snippet to prove it works in the SQL editor.

Table
Who should have access?

Each signed-in user sees and changes only their own rows.

Operations
Policies apply to
Options

Policy SQL

alter table public.todos enable row level security;

drop policy if exists "Owners can read their rows" on public.todos;
drop policy if exists "Owners can insert their rows" on public.todos;
drop policy if exists "Owners can update their rows" on public.todos;
drop policy if exists "Owners can delete their rows" on public.todos;

create policy "Owners can read their rows"
on public.todos
for select
to authenticated
using ( (select auth.uid()) = user_id );

create policy "Owners can insert their rows"
on public.todos
for insert
to authenticated
with check ( (select auth.uid()) = user_id );

create policy "Owners can update their rows"
on public.todos
for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );

create policy "Owners can delete their rows"
on public.todos
for delete
to authenticated
using ( (select auth.uid()) = user_id );

create index if not exists todos_user_id_idx on public.todos using btree (user_id);

What this allows

  • "Owners can read their rows" (select, authenticated): Users can read only rows where user_id matches their user id.
  • "Owners can insert their rows" (insert, authenticated): Signed-in users can add rows only when user_id is their own user id.
  • "Owners can update their rows" (update, authenticated): Users can change only rows where user_id is their id, and cannot change user_id to someone else.
  • "Owners can delete their rows" (delete, authenticated): Users can delete only rows where user_id matches their user id.
  • Anything not allowed by a policy is denied once RLS is on. The service role key bypasses all of this.

Test it in the SQL editor

begin;
set local role authenticated;
set local request.jwt.claims = '{"sub":"00000000-0000-0000-0000-000000000001","role":"authenticated"}';
select * from public.todos;
rollback;

begin;
set local role authenticated;
set local request.jwt.claims = '{"sub":"00000000-0000-0000-0000-000000000001","role":"authenticated"}';
insert into public.todos (user_id) values ('00000000-0000-0000-0000-000000000002');
rollback;
  • Expected: the select returns only rows where user_id = 00000000-0000-0000-0000-000000000001.
  • Expected: the second block, an insert for another user, fails with "new row violates row-level security policy". If your table has other required columns, add them to the insert.
  • Each block runs inside a transaction that ends in rollback, so nothing is saved. Apply the policies first, then run each block on its own.

Everything runs in your browser. Nothing you type is sent anywhere. Review the SQL and try it on a development project before running it on production.

The anon key is public

Your anon (publishable) key ships in the browser by design. Anyone can copy it and call your database API directly, so RLS policies are the only thing deciding what they can read or write.

Supabase docs: API keys

The service role key bypasses RLS

The service_role (secret) key ignores every policy. Keep it in server-only environment variables and never ship it to the browser or a mobile app.

Supabase docs: API keys

Views bypass RLS by default

A view runs with its creator's permissions, which usually bypass RLS. On Postgres 15 and later, create it with security_invoker = true so the caller's policies apply.

Supabase docs: Row Level Security

Storage needs its own policies

Table policies do not cover files. Access to Storage is controlled by RLS policies on the storage.objects table, written separately for each bucket.

Supabase docs: Storage access control

Never authorize with user_metadata

Signed-in users can update their own user_metadata, so a role stored there can be set by anyone. Store roles in app_metadata, which only your server can change. Claim changes reach the JWT only after it is refreshed.

Supabase docs: Row Level Security

Wrap auth functions in select

Writing (select auth.uid()) instead of auth.uid() lets Postgres evaluate it once per statement rather than once per row, and indexing the filtered column helps large tables.

Supabase docs: RLS performance recommendations

Why Row Level Security matters in Supabase

Supabase apps talk to the database straight from the browser using the anon key. That key is meant to be public, so anyone can copy it from your site and send their own queries. Row Level Security is what decides which rows those queries can see or change.

A table in the public schema with RLS turned off can be read and written by anyone through the API. Turning RLS on with no policies blocks everything for the anon and authenticated roles. Policies then open up exactly the access you want.

Which pattern should I pick?

PatternGood forWatch out for
Owner onlyNotes, todos, settings, anything personalUpdate also needs a select policy to find the row
Public read, owner writeBlog posts, public profiles, listingsEvery column is readable by anyone, so keep private fields in another table
Signed-in read, owner writeCommunity content visible to membersAnyone can sign up, so signed-in is not the same as trusted
Team or organisationB2B apps where colleagues share recordsThe members table needs its own RLS, without a policy that queries itself
Owner plus admin claimSupport or moderation accessOnly trust roles in app_metadata, never user_metadata
Insert onlyContact forms, waitlists, feedbackSpam: add constraints, a CAPTCHA or rate limit
PrivateTables only your server should touchYour server must use the service role, which bypasses RLS

How to apply and verify the policies

  1. 1Run the generated SQL in the SQL editor of a development project first.
  2. 2Paste the test snippet, replace the user id with a real id from auth.users, and run it. It impersonates that user inside a transaction and rolls back, so nothing is saved.
  3. 3Check the results match the expectations shown under the snippet, including the insert that should fail.
  4. 4Test from your app with two real accounts: sign in as one and try to load the other's data.
  5. 5Only then apply the same SQL to production.
Find public tables with RLS turned off
select tablename
from pg_tables
where schemaname = 'public'
  and not rowsecurity;

Mistakes that still leak data with RLS on

  • A policy with using (true) for authenticated on a private table: any stranger who signs up can read everything.
  • Roles stored in user_metadata: users can edit it themselves. Use app_metadata.
  • Views created without security_invoker = true, which run with their creator's permissions and skip RLS.
  • Storage buckets left without policies on storage.objects, or private files in a public bucket.
  • The service_role key in client code or a NEXT_PUBLIC_ or VITE_ variable, which bypasses every policy.
  • An update policy without with check, which lets a user hand their row to someone else by changing the owner column.

Frequently asked questions

Is my schema sent anywhere when I use this generator?

No. The generator runs entirely in your browser. Table and column names you type stay on your device and are not sent, stored or logged anywhere.

Why does the SQL use (select auth.uid()) instead of auth.uid()?

Supabase's performance recommendations suggest wrapping auth functions in a select so Postgres can evaluate them once per statement instead of once per row. The result is the same; it is just faster on large tables.

What does FORCE ROW LEVEL SECURITY do?

Normally the owner of a table bypasses its RLS policies. FORCE makes the policies apply to the owner too. Superusers and roles with the BYPASSRLS attribute, such as Supabase's service role, still bypass them.

Why can't I use a table name with spaces or dashes?

The generator only accepts letters, digits and underscores so the SQL it produces cannot be broken or altered by unusual input. Names with capitals or reserved words are wrapped in double quotes automatically.

Do these policies cover files in Supabase Storage?

No. Storage access is controlled by separate RLS policies on the storage.objects table. Write those per bucket, and keep private files in private buckets.

Does passing these tests mean my app is secure?

It means these policies behave as written for the cases you tested. Your app can still leak data through views, storage, server code using the service role, or exposed keys. Treat it as one important layer.

Sources

  1. 1.Supabase docs: Row Level Security
  2. 2.Supabase docs: Understanding API keys
  3. 3.Supabase docs: Storage access control
  4. 4.PostgreSQL docs: Row Security Policies
  5. 5.PostgreSQL docs: CREATE POLICY