Vibe coding security

Supabase RLS checklist for AI-built apps

Supabase is easy to start with and easy to leave wide open. This checklist explains Row Level Security, why the anon key is safe to expose, and how to confirm your tables are not readable or writable by the public.

Short answer

To secure a Supabase app before launch: enable Row Level Security (RLS) on every table holding real data and add policies that match who should access each row. The public anon key is designed to be exposed — your policies, not the key, are what protect your data. Keep the service_role key server-side only, because it bypasses RLS entirely. Then test as both an anonymous and a logged-in user to confirm each sees only what they should.

Key takeaways

  • The Supabase anon key is designed to be public — your table policies are what keep data safe.
  • Enable Row Level Security on every table that holds real data, and add policies that match who should see each row.
  • The most common mistake is a table that allows public reads or writes it should not.
  • The service_role key bypasses all RLS and must stay server-side — treat any exposure as critical.
  • A scanner can spot an exposed key or an obviously open endpoint, but it cannot prove your policies are correct.

What is Row Level Security?

Row Level Security (RLS) is a PostgreSQL feature that Supabase puts front and center. It decides access one row at a time. With RLS enabled, a query through the public key only returns or changes rows that a policy explicitly allows — for example, only rows where user_id equals the logged-in user's id.

The default matters: when you turn RLS on but write no policies, the table denies all access through the anon key. That is the safe starting point. You then add narrow policies that permit exactly the access your app needs — no more.

Why the Supabase anon key is not a secret

Supabase gives you two main keys. Understanding the difference is the single most important thing for getting this right.

The anon (public) key

This key is meant to ship in your frontend and be visible to every visitor. It is not a password. On its own it grants only the access your RLS policies allow. Finding it in your bundle is expected and normal — it is not a finding to panic over.

The service_role key

This key bypasses RLS completely and has full access to your database. It must only ever be used on the server. It should never appear in your frontend, a public file, or your browser bundle.

In short: the anon key being public is by design, but your table policies must be correct. The protection lives in the policies, not in hiding the public key.

The common mistake: public reads and writes on sensitive tables

The mistake that causes real leaks is a table that is readable — or worse, writable — by anyone with the anon key. This happens when RLS is left off, or when a policy is far broader than intended (for example, a policy that allows any request to select every row).

With RLS misconfigured, a stranger using your public key could:

  • Read every row of a table — including other users' private data.

  • Insert, update, or delete rows they should never be able to touch.

  • Enumerate your entire dataset, one query at a time.

This is the database equivalent of leaving the back door open. The app still works perfectly in a demo, which is why it so often ships unnoticed.

Quick manual checks inside Supabase

You can verify most of this from the Supabase dashboard in a few minutes. No code required.

  • Open the Table Editor and confirm RLS is enabled on every table that holds real or private data.

  • Open Authentication → Policies and read each policy out loud — does it allow exactly who you intend, and nothing broader?

  • Look for policies that apply to the anon or public role on sensitive tables, and confirm that is intentional.

  • Confirm write policies (insert, update, delete) are scoped to the right user, not open to everyone.

  • Test as a logged-out user: a query against a private table should return nothing or an error, not data.

  • Test as a logged-in user: confirm you can see your own rows but not another user's rows.

Database permissions are one section of the broader vibe coding security checklist and a hard requirement on the final launch security checklist. For the broader access boundary — login flows, sessions, admin and per-user authorization — see the authentication security checklist.

What service_role exposure means

If your service_role key is exposed anywhere public, it is a critical problem. That key ignores every RLS policy and can read and write your entire database. A common way it leaks is being placed in a frontend environment variable or committed to a file that ends up public — the same way other secrets leak, covered in public .env files and what to do if secrets leak.

  • Rotate the service_role key in your Supabase dashboard immediately.

  • Remove it from any public file, bundle, or repository history.

  • Confirm it is only ever read from server-side environment variables.

  • Review your database and auth logs for unexpected activity.

What GuardMint can and cannot verify

GuardMint can flag the outward signs of a Supabase misconfiguration from your public surface. Our methodology explains how the public scan works.

  • Detects Supabase keys present in your frontend, and flags a service_role key as a serious finding.

  • Identifies your Supabase project URL and obvious public exposure in the bundle.

  • Reports related issues like leaked secrets and open endpoints.

What a scan cannot fully verify

A scanner cannot log into your database or read your policies. It cannot prove that your RLS rules are correct, that each policy is scoped to the right user, or that a given table is safe — those depend on logic only you can see. Seeing an anon key is expected and is not proof of a problem. GuardMint is a launch-readiness check, not a penetration test, audit, or compliance certification: use it to catch exposed keys and obvious mistakes, and verify your policies yourself using the manual checks above. See our disclaimer for full scope.

Frequently asked questions

Is the Supabase anon key safe to expose?
Yes. The anon (public) key is meant to be included in your frontend and seen by users. It only grants the access your Row Level Security policies allow. The key is not the secret — your policies are the protection. The key you must never expose is the service_role key.
What is Row Level Security (RLS)?
RLS is a Postgres feature, exposed in Supabase, that controls access row by row. With RLS enabled and policies written, a request can only read or change the rows the policy permits — for example, only rows where the user_id matches the logged-in user. Without policies, an RLS-enabled table denies access by default.
How do I secure a Supabase app before launch?
Enable RLS on every table with real data, write policies that match who should access each row, keep the service_role key server-side only, and test as both an anonymous and a logged-in user to confirm each one sees only what they should. Then re-check after any schema or policy change.
I enabled RLS but added no policies. What happens?
With RLS enabled and no policies, the table denies all access through the anon key by default. That is safe, but your app's legitimate queries will also fail. You then add specific policies to allow exactly the access you intend.
What if my service_role key was exposed?
Treat it as critical. The service_role key bypasses RLS entirely, so anyone holding it has full read and write access to your database. Rotate it immediately in your Supabase dashboard, remove it from any public location, and review your logs for unexpected activity.

Check your Supabase app for exposed keys

Run a free security scan on your live app — GuardMint flags Supabase keys in your frontend and other obvious exposure. No signup required for your first score.

Supabase RLS Checklist for AI-Built Apps | GuardMint