VibeSecurity

Platform checklist

Supabase security checklist for AI-built apps

Apps generated by AI tools very often use Supabase as the whole backend, with the browser talking to the database directly. That makes your Postgres policies the only thing between the internet and your data. By the end of this checklist you will have confirmed, with your own queries, that each table, bucket, view and function enforces who can do what.

By the VibeSecurity team6 min read

Where the trust boundary is

Supabase exposes your database over an HTTP API. Requests arrive as one of two Postgres roles: anon for visitors with no session, and authenticated for signed-in users. The publishable key (or the legacy anon key) is designed to sit in your front end, and Supabase states that publishable keys are low privilege and subject to Row Level Security. Your protection is therefore whatever RLS policies, grants and function permissions say.

The secret key (or the legacy service_role key) bypasses RLS entirely. Supabase documents it as backend-only, and says it does not work in a browser. If a secret key ever reaches client code, a repository or a chat, treat the whole database as exposed and rotate it.

For the general concept of why browser code cannot enforce access, see our guides on environment variables and client-side secrets. This page is only about what to check inside Supabase.

The checklist

Supabase review areas
AreaWhat to verifyHow to test on your own projectPass condition
RLSEvery table in an exposed schema has RLS enabled and a policy per operationRun the RLS query in Step 1, then open the Security AdvisorNo table listed with rls off; no policy that reads using (true) for writes
PoliciesPolicies name a role and tie rows to the callerRead each policy; sign in as user A and request user B's row idSecond user gets zero rows or an error
KeysOnly the publishable or anon key is in the bundleSearch the built JS and repository for sb_secret_ and service_roleNo secret key anywhere client-side or in git history
StoragePrivate buckets have policies on storage.objects scoped to the userRequest an object URL logged out; try listing another user's folderLogged-out and cross-user requests fail
Auth settingsSignups, email confirmation, anonymous sign-ins and redirect URLs match your intentReview Authentication settings and URL configuration in the dashboardOnly intended sign-in paths are on; redirect list has no wildcards you do not own
Edge functionsJWT verification is on unless the function is a webhook with its own checkCall the function with no Authorization headerPrivate functions reject the call
Views and functionsViews use security_invoker; security definer functions are not callable by anonRun the advisor and the view query in Step 5No security definer views; no exposed auth.users data

Step 1: Find tables without RLS

Open the SQL editor and list tables in the public schema with their RLS flag. Any row where rowsecurity is false is readable and writable by anyone who has your project URL and public key, subject to grants.

List tables and RLS status
select schemaname, tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;

Step 2: Read and write policies

Enabling RLS with no policies denies everything, which is safe but breaks the app, and generated code often fixes that by adding a policy with using (true). Supabase recommends separate policies for select, insert, update and delete, and naming the role with the to clause. A policy that is true for anon on a table of user data is a public table.

Write ownership policies against the signed-in user. Wrapping auth.uid() in a select lets Postgres evaluate it once per query, which the Supabase advisor also recommends.

Owner-only policies
alter table public.notes enable row level security;

create policy "read own notes" on public.notes
for select to authenticated
using ((select auth.uid()) = user_id);

create policy "insert own notes" on public.notes
for insert to authenticated
with check ((select auth.uid()) = user_id);

Step 3: Prove it from outside

Use the same request a stranger would. Substitute your project reference and public key, and run it against a table that should be private. You should get an empty array or an error, never rows.

Logged-out read attempt against your own project
curl -s "https://PROJECT_REF.supabase.co/rest/v1/notes?select=*&limit=5" \
  -H "apikey: $SUPABASE_PUBLISHABLE_KEY" \
  -H "Authorization: Bearer $SUPABASE_PUBLISHABLE_KEY"

Step 4: Storage and keys

Supabase Storage does not permit uploads to a bucket without an RLS policy on storage.objects. Public buckets bypass RLS, so anything in one is fetchable by URL. Put user documents in private buckets and scope policies to a folder named after the user.

For keys, search your repository and its history for secret-key prefixes, and check the deployed bundle. If a secret key was exposed, Supabase's documented rotation is: create a replacement, update every component that used it, confirm nothing still uses the old one, then delete it.

Per-user storage policy and a key search
create policy "users read own files" on storage.objects
for select to authenticated
using (
  bucket_id = 'documents'
  and (storage.foldername(name))[1] = (select auth.jwt()->>'sub')
);

git log -p --all -S"sb_secret_" | head -50
grep -rnE "sb_secret_|service_role" src dist

Step 5: Views, functions and the advisor

A view runs with its owner's rights by default, which can bypass RLS on the tables underneath. On Postgres 15 and later, create views with security_invoker so the caller's policies apply. Functions marked security definer also bypass RLS, so revoke execute from anon and authenticated unless you mean to expose them.

Supabase's Security Advisor checks all of this. It reports RLS disabled in public, security definer views, exposed auth users and sensitive columns without RLS. Run it after every schema change.

Safe view and advisor run
create view public.my_notes
with (security_invoker = true)
as select id, title from public.notes;

supabase db advisors

Step 6: Auth settings and edge functions

In the dashboard, confirm whether new signups are allowed, whether email confirmation is required, and whether anonymous sign-ins are on. If you are building an internal tool, disable signups. Review the redirect URL allow list so sign-in links cannot be sent to a domain you do not control.

Edge functions verify the caller's JWT by default. Setting verify_jwt to false is legitimate for webhooks that authenticate another way, such as a signature, but it must be a deliberate choice. Inside a function, use the client scoped to the user so RLS still applies, and use the admin client only after your own authorization check.

Call a private function without credentials
curl -i -X POST "https://PROJECT_REF.supabase.co/functions/v1/my-function" \
  -H "Content-Type: application/json" \
  -d '{}'

Common mistakes

  • Adding using (true) to make an error go away.
  • Putting the secret key in a front-end environment variable because a tutorial did.
  • Enabling RLS on the table but leaving a view over it without security_invoker.
  • Trusting a user id sent in the request body instead of auth.uid().
  • Marking a bucket public for convenience during development and never reverting.
  • Testing only as the owner account, so cross-user leaks never show up.

Keep it working

Re-run the RLS query, the advisor and the logged-out request after every migration or AI-generated schema change. Create two test accounts and keep them for regression checks.

Frequently asked questions

Is it a problem that my Supabase URL and public key are visible in the browser?

No. They are meant to be public. What matters is that RLS is enabled with real policies on every exposed table, because the public key only gets the access your policies allow.

Does enabling RLS break my app?

It denies all access until policies exist, so expect errors at first. Add narrow policies for each operation and role, then test as a normal user. Do not respond by adding a policy that allows everyone.

When is the secret or service_role key acceptable?

Only on servers you control, such as edge functions or backend jobs that do their own authorization checks. Never in a browser bundle, mobile app or public repository.

Should I turn off signups?

If the app is for a fixed set of people, yes. Open signups plus permissive policies means strangers can create accounts and read what authenticated users can read.

Sources

  1. 1.Supabase docs: Row Level Security
  2. 2.Supabase docs: API keys
  3. 3.Supabase docs: Storage access control
  4. 4.Supabase docs: Database advisors
  5. 5.Supabase docs: Edge Functions auth
  6. 6.Supabase docs: Auth general configuration