TentyAll guides

Supabase keys and tables

Supabase apps put a public key in the page on purpose. Row level security is the lock that makes that safe.

What it is

The anon key and the publishable key are public by design. They let the browser talk to your database, and row level security decides which rows each visitor can see. The service_role key and the secret key skip every rule, so they belong on a server only.

Why it matters

A service_role or secret key in a page hands out every row and every change to anyone. A table without row level security hands out its rows to anyone who holds the public key. Anonymous sign in gives each visitor the authenticated role, so a policy that trusts authenticated users also trusts every stranger. Tenty rates a bypass key critical, and a readable table high, or critical when its columns look personal.

How Tenty checks it

Tenty reads the keys and calls in your scripts on every scan. For verified owners it then sends read-only requests to the REST address of each Supabase project named in your site, for up to 25 tables per project. It sends the public key and asks for one row. The finding names the table, its columns and the row count. Row values stay out of the report.

How to fix it

  • Bypass keys: rotate the JWT secret in Supabase, remove the key from the front end and use it in server code only.
  • Readable tables: turn on row level security for the table and add a policy that names who may read, for example using (auth.uid() = user_id).
  • Anonymous sign in: write policies that check auth.jwt() ->> 'is_anonymous' when the rows should belong to real accounts.
  • Admin calls in the browser: move auth.admin calls to a server function.

How to reproduce

curl -sS -H 'apikey: YOUR_PUBLIC_KEY' 'https://PROJECT.supabase.co/rest/v1/TABLE?select=*&limit=1'

Use the project, table and public key from your own page. A JSON row in the answer means the table is readable. An empty list or an error means the policy holds for that key.