Security

    Why does Supabase RLS fail silently in mobile apps?

    A 2026 diagram showing a mobile app's public Supabase anon key being used to query the REST API directly, where a missing Row Level Security policy silently returns all rows with no error

    The dangerous thing about a Supabase Row Level Security mistake is that nothing breaks. A denied query returns an empty result, not an error, so a policy that is too strict looks like a missing-data bug, and a policy that is too loose leaks data with no warning at all. In a mobile app, where the anon key ships in the bundle, that silence is the whole problem. Here is why RLS fails quietly and how to make it speak up.

    Short answer

    Supabase RLS fails silently because a denied query returns an empty result rather than an error. Supabase notes that a policy like using (auth.uid() = user_id) silently denies unauthenticated users, since null = user_id is always false, so a too-strict policy reads as a no-data bug and a too-loose one leaks data quietly. This matters most in mobile apps, because the anon key is public in your bundle and anyone can call the REST API directly, which makes RLS the only guard on your data. Supabase requires RLS on every table in the public schema, and the only safe move is to test policies, not assume.

    What you should know

    • Denials return empty, not errors: RLS does not throw on a blocked read, so failures look like missing data.
    • Two silent modes: a too-strict policy locks data out, and a too-loose one leaks it, both without warning.
    • The anon key is public: it ships in your mobile bundle, so anyone can query your REST API directly.
    • RLS is the only guard there: with the key exposed, your policies are what stand between a stranger and your data.
    • You must test it: empty results do not prove safety, so verify policies as the anon and authenticated roles.

    Why is an RLS failure silent?

    Because Postgres evaluates a row-level policy as a filter, not a gate that raises an error. When a policy denies a row, that row is simply excluded from the result, so a blocked read returns zero rows the same way a genuinely empty table would. Supabase points out the classic case: a policy of using (auth.uid() = user_id) silently fails for an unauthenticated request, because auth.uid() is null and null = user_id is always false in SQL. There is no exception, no status code on the read path, just an empty array. That is convenient for the database and dangerous for you, because the absence of an error is not the presence of safety.

    What does a silent leak look like next to a silent lockout?

    They are opposite mistakes with the same calm appearance. A silent lockout happens when RLS is on with a missing or too-strict policy, so legitimate users see nothing and the app looks broken. A silent leak happens when RLS is off or a policy is too permissive, so the data flows normally and nobody notices that anyone can read it. The table sets them side by side.

    Silent failureCauseWhat you seeReal risk
    Silent lockoutRLS on, policy too strict, or null auth.uid()Empty results, no errorApp looks broken, tempting you to over-loosen the policy
    Silent leakRLS off, or a policy like using (true)Data returns normallyAnyone with the anon key can read every row

    The trap that connects them is the fix for the first that creates the second: a developer loosens a policy to make the empty results go away, and turns a lockout into a leak.

    Why does this matter more in a mobile app?

    Because the anon key is not a secret, and your app proves it. Supabase publishes the anon key for client use, so it is embedded in your mobile bundle and anyone can extract it, point a tool at your project URL, and query the REST API directly. They never touch your app's interface, so any filtering you do in the UI is irrelevant. RLS is the only layer that decides what that raw API call returns. Supabase is explicit that RLS must always be enabled on any table in an exposed schema, which is the public schema by default, precisely because the key that reaches those tables is public.

    How do you test RLS so the failures are not silent?

    Make the database tell you what the API would return. Three checks cover most of it. First, run the Supabase Security Advisor, which flags tables in the public schema that have RLS disabled. Second, query each table with the anon key and with a test user, and confirm you can read your own rows and get an empty result for someone else's, rather than assuming. Third, write the policy to check authentication explicitly so an unauthenticated request is denied on purpose, not by accident:

    alter table profiles enable row level security;
    
    create policy "Users read only their own profile"
      on profiles for select
      to authenticated
      using ( (select auth.uid()) = user_id );
    

    For deeper coverage, Supabase points to testing policies with pgTAP so the rules are verified on every change rather than by hand.

    What to watch out for

    The first trap is reading empty results as proof of safety; they often mean the opposite, a policy you have not finished. The second is the separate-policy gotcha: a read needs a USING policy and a write needs a WITH CHECK policy, so an ALL policy can pass one path and silently block the other. The third is mobile-specific: because the anon key is in your binary, the only honest test is the one that uses it the way an attacker would. A pre-submission scan such as PTKD.com (https://ptkd.com) reads the compiled APK, AAB, or IPA, recovers the Supabase URL and anon key, and checks against OWASP MASVS whether your tables answer an unauthenticated request, which is exactly the silent leak this article is about. The limit is that a scan confirms exposure; you still write the policies that close it.

    What to take away

    • Supabase RLS fails silently: a denied query returns an empty result, so leaks and lockouts both look calm.
    • A too-strict policy locks users out and a too-loose one leaks data, and fixing the first carelessly creates the second.
    • In a mobile app the anon key is public in your bundle, so RLS is the only guard on the data your REST API returns.
    • Test policies with the Security Advisor and as the anon role, and verify the build with a pre-submission scan such as PTKD.com that probes whether your RLS is actually leaking.
    • #supabase-rls
    • #row-level-security
    • #silent-failure
    • #anon-key
    • #data-leak
    • #mobile-security
    • #owasp-masvs

    Frequently asked questions

    Why does my Supabase query return empty with no error?
    Because RLS denies rows by filtering them out, not by raising an error. A blocked read returns zero rows the same way an empty table would. A common cause is a policy like using (auth.uid() = user_id) on an unauthenticated request, where auth.uid() is null and the comparison is always false. Check that the user is authenticated and that a SELECT policy actually permits their rows.
    Can Supabase RLS leak data without showing an error?
    Yes, and that is the dangerous case. If RLS is disabled on a public-schema table or a policy is too permissive, such as using (true), the data returns normally and nothing flags it. Anyone with the public anon key can then read every row through the REST API. The lack of an error is not safety, so a leak looks identical to a working app until someone finds it.
    Why is RLS critical when my anon key is public?
    Because the anon key is meant to be public and ships in your mobile bundle, so anyone can extract it and query your project's REST API directly, bypassing your app entirely. Any filtering in your interface does not apply to that raw call. RLS is the only layer that decides what the API returns, which is why Supabase requires it on every table in the exposed public schema.
    My app shows the right data, so my RLS is fine, right?
    Not necessarily. Your app's interface filters what it displays, but the REST API does not, and an attacker uses the API with your public anon key rather than your UI. A screen that looks correct can sit on top of a table that returns everything to a direct query. The only honest check is to query each table as the anon role and confirm it denies other users' rows.
    How do I test my Supabase RLS policies?
    Run the Security Advisor to flag tables with RLS disabled, then query each table with the anon key and a test user to confirm you read only your own rows. Write policies that check authentication explicitly so unauthenticated requests are denied on purpose. For ongoing coverage, test policies with pgTAP so they are verified on every change rather than checked once by hand.

    Keep reading

    Scan your app in minutes

    Upload an APK, AAB, or IPA. PTKD returns an OWASP-aligned report with copy-paste fixes.

    Try PTKD free