Is your Supabase anon key safe in an Android app?
Updated
Short answer: the anon key, or the newer publishable key, is meant to be in your app. Whether that is safe depends entirely on Row Level Security. The service_role key, or the newer secret key, must never be in an app at all.
Your APK is readable by anyone who downloads it, so assume every key in it is public. See how to check your APK for leaked keys.
Which Supabase key is which?
Supabase has two kinds of key, each in a current and a legacy form:
- Publishable key (
sb_publishable_...), or the legacy anon key. Supabase describes it as "safe to expose online", including in a "mobile or desktop app". Requests made with it act as theanonrole, or asauthenticatedonce the user signs in. - Secret key (
sb_secret_...), or the legacy service_role key. "Only use in backend components of your app." Supabase lists mobile apps, "where the key is bundled inside the compiled packages", among the places it must never appear.
Supabase refuses secret keys from browsers by checking the User-Agent. An Android app is not a browser, so that check does not protect you: a secret key in an APK works for whoever extracts it.
What actually protects the data?
Row Level Security. With the publishable key, Postgres decides what each request may read or change, table by table, using the policies you write.
Supabase's documentation is blunt about the two states that matter:
- "A table in an exposed schema without RLS is readable and writable by any role with a grant on it."
- "Once RLS is enabled, no data is accessible through the API when using a publishable key, until you create policies."
So RLS off means open to anyone with your project URL and the key from your app. RLS on with no policies means closed, which is safe but breaks your app until you add policies.
How do I check every table?
Two ways, and it is worth doing both.
The Security Advisor, in your project's dashboard, runs Supabase's own
checks. The one that matters here is rls_disabled_in_public, which it rates as
an error: "Anyone with your project URL can read, edit, and delete all data in
this table because Row-Level Security is not enabled."
A query in the SQL editor lists every table in the public schema and
whether RLS is on:
select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by tablename;
Any row with rowsecurity false is a table your app's key can read and write
freely.
What does a good policy look like?
A policy says which rows a role may touch. The pattern Supabase uses in its own examples, letting each signed-in user read only their own profile:
alter table profiles enable row level security;
create policy "Users can view their own profile."
on profiles for select
to authenticated
using ( (select auth.uid()) = user_id );
Write one policy per action you allow (select, insert, update, delete), and allow nothing else.
Which mistakes open the data up anyway?
- Tables created in SQL. In Postgres, a new table has RLS off until you run
alter table ... enable row level security. Migrations and AI-written SQL often leave it out. using (true)for theanonrole. Supabase warns that such a policy "grants every unauthenticated visitor read access to every row". Use it only for data meant to be public.- Views. A view runs with its owner's rights and ignores the policies on the
tables under it, unless it is created
with (security_invoker = on). The Security Advisor flags these assecurity_definer_view. - The service_role key in the client to "make it work" when a policy was blocking a request. That turns every policy off for anyone holding your APK.
And if the secret key is already in a build?
Rotate it in the Supabase dashboard first, because every copy of that build still holds the old one. Then move the operation that needed it to your server or an Edge Function, where the secret stays.
This is one of the checks on the Google Play pre-launch checklist. Google's review does not check your database rules, but anyone who downloads your app can.