Platform security

Supabase security audit: RLS, API keys, storage and RPC checklist

Supabase security audit for your own project: an 8-step checklist for RLS, publishable and secret keys, views, storage and RPC, with example policies.

Short answer

A Supabase security audit checks five things: Row Level Security on every exposed table, which key ships in the client, grants to the anon and authenticated roles, views and functions that bypass RLS, and storage policies. Test each as an anonymous caller and as a second user. Keep the secret key on the server.

A Supabase security audit starts with one fact: your database is reachable from the browser. Supabase exposes tables through an API, so Row Level Security (RLS) policies are your access control. If a policy is missing or wrong, anyone with your public key can read or write data.

This page gives an eight-step test checklist and safe example policies. Run the tests only on projects you own or have written permission to test.

What is Row Level Security, and why does it decide everything?#

RLS is a Postgres feature that limits which rows a role can read or change. Supabase's docs state the main rule: "Enable RLS on every table in an exposed schema." A table in an exposed schema without RLS is readable and writable by any role with a grant on it.

Once you enable RLS, no data is accessible through the API with a publishable key until you create policies. That default-deny state is the safe one. Watch for three mistakes: RLS never enabled, a policy that is too loose, and a path that skips RLS.

Which API key should ship in the client?#

Only the publishable key (or the legacy anon key) belongs in client code. Supabase's API key page lists four types:

Key Role Safe in a browser or app? RLS applies?
Publishable (sb_publishable_...) anon, or authenticated when signed in Yes Yes
anon (legacy JWT) Same as publishable Yes Yes
Secret (sb_secret_...) service_role No. Backend only No, it bypasses RLS
service_role (legacy JWT) service_role No. Backend only No, it has BYPASSRLS

The docs say a secret key provides "full access to your project's data, bypassing Row Level Security" and must never go into "a browser, a shipped application, or source control." Supabase also says it plans to retire the legacy JWT keys by the end of 2026. Check the docs for your migration date.

How do you test a Supabase project? A checklist#

Use two test users, A and B, and an anonymous caller. Run the checks in order.

1. Find the keys in the shipped client.#

  • Search the JS bundles for service_role, sb_secret_, and long JWTs. Decode any JWT and read its role claim.
  • Confirm only a publishable or anon key appears.
  • Check git history, .env files committed by mistake, and mobile app builds.

2. List tables as an anonymous caller.#

  • Request each table with only the public key and no user token.
  • Record which tables return rows, and compare to what should be public.
# Anonymous read test against your own project (replace the placeholders)
curl -s "https://YOUR-PROJECT.supabase.co/rest/v1/notes?select=id&limit=1" \
  -H "apikey: YOUR_PUBLISHABLE_KEY"

An empty array [] can mean "no rows" or "RLS blocks you". Add a row you own first, then retest, so you can tell the two apart.

3. Test cross-user access (IDOR).#

  • As user A, create a record. As user B, request A's record by ID.
  • As user B, try PATCH and DELETE on A's record. Then read it back as A to check it did not change. A 204 response alone is not proof, because an RLS-filtered write can match zero rows.
  • Try to insert a row with user_id set to A's ID.

See IDOR testing and multi-tenant isolation testing for the method.

4. Check grants.#

  • List grants for anon and authenticated on each table. Supabase says to revoke all grants, then grant back only what each role needs.
  • Look for INSERT, UPDATE, DELETE grants that no feature uses.

5. Check views.#

  • List views in exposed schemas. Views bypass RLS by default.
  • On Postgres 15 and later, check for security_invoker = true.

6. Check functions (RPC).#

  • List functions in exposed schemas. PostgREST exposes them as /rpc/function_name.
  • Postgres grants EXECUTE on new functions to PUBLIC by default. Check who can call each one.
  • Review each SECURITY DEFINER function. It runs with its owner's rights and can bypass RLS. It needs its own checks of auth.uid() and its inputs.

7. Check storage.#

  • List buckets and mark which are public. A public bucket serves objects to anyone with the URL.
  • Review policies on storage.objects. Supabase notes that by default, storage allows no uploads without RLS policies.
  • Test upload, read, overwrite and delete as an anonymous caller, as user A and as user B, on A's path.

8. Check auth settings.#

What do safe example policies look like?#

The policies use an example notes table. Adapt the names and run them on a test database first.

-- Enable RLS and revoke broad default grants, then grant back only what is needed
alter table public.notes enable row level security;
revoke all on public.notes from anon, authenticated;
grant select, insert, update, delete on public.notes to authenticated;

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

-- Owners can insert rows only for themselves
create policy "notes_insert_own" on public.notes
  for insert to authenticated
  with check ( (select auth.uid()) = user_id );

-- Owners can update their rows, and cannot hand them to someone else
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 );

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

-- Views must run with the caller's permissions (Postgres 15+)
create view public.my_notes with (security_invoker = true) as
  select id, title, created_at from public.notes;

Supabase's docs use the wrapped (select auth.uid()) form, which lets Postgres cache the value once per statement. When no user is signed in, auth.uid() returns null, so the equality fails and access is denied.

How do you write automated RLS tests?#

Supabase documents pgTAP tests for RLS, run with supabase test db. Create a test file with supabase test new notes_rls. The docs also give three rules to copy:

  • Switch role and identity with set local role and set local request.jwt.claim.sub.
  • Do not prove an allowed write with lives_ok, because it also passes when the write matched zero rows. Return the row and assert on it.
  • After a denied write, check that the target row is unchanged.

Run the tests in your CI pipeline, so a migration that drops a policy fails the build.

What are the common Supabase mistakes?#

Look for these mistakes in a Supabase-backed app:

  • RLS never enabled. A table was created from the dashboard or a migration without enable row level security.
  • Policy using (true). It reads as "allow all" and exposes every row to the role it names.
  • Insert without a check. A policy that has using but no with check on insert or update lets a user write a user_id that is not theirs.
  • Trusting user-editable metadata. Policies that read user_metadata from the JWT can be changed by the user. Use server-controlled data for roles.
  • Secret key in a front-end bundle. A build tool inlined the wrong variable.
  • Edge Functions that skip checks. A function uses the secret key, so RLS no longer applies, and it does not verify the caller.
  • Public buckets for private files. Invoices or ID photos sit in a bucket that serves any URL.

The checklist above has a test for each mistake. Add a unit test for each fix, so it stays fixed.

How should you handle credentials during a test?#

Use test users and test data. If you suspect a secret key has shipped in a client, treat it as exposed: rotate it, then review project logs for use. Never paste a real key into a ticket or chat. To prove exposure, show the key's type and where it appeared, with the value masked.

What do testers report?#

A good finding names the table, the role, the request and the proof. For example: "As an anonymous caller, GET /rest/v1/invoices returned rows from other tenants. Cause: RLS is off on public.invoices. Fix: enable RLS, add owner-scoped policies, revoke default grants." Evidence includes the request, the response, and a read-back from the owning account.

For platform-specific risks across Bubble, Supabase and Firebase apps, see no-code app security testing. The matching Firebase guide is Firebase Security Rules testing.

Questions

Is the Supabase anon key a secret?

No. The anon key (legacy) and the newer publishable key are designed to ship in client code. Supabase says they only reach what Row Level Security allows. That makes your RLS policies the real access control. The service_role and secret keys must stay on the server.

Why can users still read a table after I added a policy?

Policies decide row access only if RLS is on, and table grants still apply. Supabase documents that adding policies does not take back default grants to anon and authenticated. Review grants, and enable RLS before you rely on any policy.

Do Supabase views respect RLS?

Not by default. Supabase says views bypass RLS by default because they are usually created by the postgres user. On Postgres 15 and later, create the view with security_invoker = true so the view uses the caller's permissions.

Can I test RLS without hitting production?

Yes. Use a local or branch database and the Supabase CLI. Supabase documents pgTAP tests for RLS, run with supabase test db. Test with the same roles and JWT claims that real callers use.

Sources

  1. Supabase Docs: Row Level Security (checked 9 November 2026)
  2. Supabase Docs: Understanding API keys (checked 9 November 2026)
  3. Supabase Docs: Storage access control (checked 9 November 2026)
  4. Supabase Docs: Database functions (checked 9 November 2026)
  5. PostgREST: calling functions (RPC) (checked 9 November 2026)
  6. PostgreSQL: CREATE FUNCTION (privileges and SECURITY DEFINER) (checked 9 November 2026)