AI-coded apps

    Supabase Anon Key vs Service Role Key Explained

    A comparison of the Supabase anon key going into the frontend under Row Level Security and the service_role key staying on the backend.

    The Supabase anon key and the service_role key differ in one decisive way: the anon key is public and constrained by your Row Level Security policies, so it belongs in your frontend, while the service_role key bypasses Row Level Security entirely and has full access to your data, so it must live only on a server and never in your app. The anon key going into client code is expected and safe when Row Level Security is enabled; the service_role key in client code is a full compromise. So the answer to which key goes in the frontend is the anon key, always, and the reason the service_role key cannot is precisely that it ignores the policies that make the anon key safe.

    Short answer

    Put the anon key in the frontend and keep the service_role key on the server. Per Supabase's API keys documentation, the anon or publishable key is designed for client use and the service_role key is a secret with high-privilege access. Per Supabase's Row Level Security docs, the anon key is constrained by policies, so it can only do what you allow, while the service_role key bypasses Row Level Security and can read, change, or delete anything. So the frontend gets the anon key, always, and the service_role key never ships in your app. If a service_role key has leaked, rotate it immediately.

    Which key goes in the frontend?

    The anon key goes in the frontend, and the service_role key never does. Supabase apps talk to the database directly from the client using the anon, or publishable, key, which is designed to be public and shipped in your app, so including it in your web or mobile frontend is the intended, supported setup. Because it is meant to be visible, you do not need to hide it, and hiding it would not be possible anyway since client code is inspectable.

    The service_role key is the opposite: it is a secret meant only for trusted server environments, and putting it in your frontend hands anyone who inspects your app full control of your database. So the placement rule is simple and absolute. Any code that runs on the user's device or in their browser, your React, React Native, or web app, uses the anon key. Any operation that needs the service_role key runs on a server you control, whether your own backend or a Supabase Edge Function, and the key stays there. If you are unsure which key a piece of code should use, the fact that it runs on the client answers it: the anon key.

    What is the anon key

    The anon key is Supabase's public, client-facing key, and its safety comes entirely from Row Level Security. It carries no privileges on its own; every request made with it is subject to the Row Level Security policies on your tables, so it can only read or write the data those policies permit. That is why it is safe to embed in your app: with Row Level Security enabled and correct, the anon key is powerless beyond what you have explicitly allowed, even though anyone can see it.

    The critical dependency is Row Level Security. The anon key is only as safe as your policies, so a table with Row Level Security disabled is fully open to the anon key, and a policy that grants everyone access is equally open. This is the root of most Supabase exposure incidents: not that the anon key leaked, since it is public by design, but that the database behind it had no policies constraining what that public key could reach. So treating the anon key as safe is correct only when you have paired it with Row Level Security on every table it can touch.

    What is the service_role key, and the RLS bypass

    The service_role key is Supabase's secret, high-privilege key, and its defining property is that it bypasses Row Level Security entirely. Where the anon key is checked against your policies on every request, the service_role key skips those checks and has full read and write access to all your data, which is by design so that trusted server code can perform administrative and privileged operations. That power is exactly why it is dangerous outside a server.

    Because it ignores Row Level Security, no policy protects you if this key is exposed. An attacker with your service_role key can read every row, modify anything, and delete your data, regardless of how carefully you wrote your policies, because those policies do not apply to it. This is the single most important distinction between the two keys: your Row Level Security is the wall that contains the anon key, and the service_role key walks straight through that wall. So it must live only where you trust the environment completely, a server you control, and never in an app, a repository, or anywhere a client or a third party could obtain it.

    Safe versus unsafe: the placement rule

    The safe-versus-unsafe line follows directly from how the two keys relate to Row Level Security. The anon key is safe in the client because policies constrain it, so shipping it in your frontend is fine as long as Row Level Security is enabled and correct on the tables it reaches. Keeping the anon key out of your app would be pointless, since it is public by design and your protection lives in the policies, not in concealing the key.

    The service_role key is unsafe anywhere a client or attacker could reach it, because nothing constrains it. It belongs exclusively on a server, and it must never appear in your app bundle, your repository, a committed configuration file, an environment variable exposed to the client, or a chat or ticket. The same applies to other true secrets like your database connection string. So the rule to remember is that the anon key ships to clients under the protection of Row Level Security, and the service_role key stays server-side because it has no such protection.

    If you exposed the service_role key: rotate

    If your service_role key has been exposed, in your app, your repository, or anywhere public, treat it as a full compromise and rotate it immediately, because it grants complete access that no policy limits. In your Supabase dashboard, roll the service_role key so the exposed value stops working, then update your server-side code to use the new key. Do this before anything else, since the old key remains fully dangerous for as long as it is valid.

    After rotating, remove the key from wherever it leaked and move its operations to a proper server if they were not already, because reissuing the key and shipping it the same way just exposes a new secret. Review your database and logs for any signs it was used, since an exposed service_role key means an attacker could have read or altered anything. The anon key, by contrast, does not need rotating on exposure, because it is public by design and safe as long as your Row Level Security is in place; only the service_role key is an emergency.

    Anon versus service_role compared

    Setting the two keys side by side makes the decision obvious. The table below compares them.

    AspectAnon or publishable keyservice_role or secret key
    Public or secretPublic, ships in the clientSecret, server-side only
    Row Level SecurityGoverned by your policiesBypasses Row Level Security entirely
    Access it grantsOnly what policies allowFull read and write to all data
    Where it belongsThe frontendA server you control
    If it leaksSafe when Row Level Security is onFull compromise, rotate immediately

    Read the table by the Row Level Security row: it is the whole story, since the anon key is contained by policies and the service_role key ignores them.

    Verification checklist

    Working through these steps confirms your keys are placed and protected correctly. The checklist below covers them.

    StepActionDone?
    Frontend keyOnly the anon key in client code[ ]
    Enable Row Level SecurityOn every table the anon key can reach[ ]
    Service_role server-sideNever in the app, repository, or client config[ ]
    Rotate if exposedRoll a leaked service_role key at once[ ]
    Verify the bundleNo service_role key in the shipped app[ ]
    Test the anon keyConfirm it cannot reach data it should not[ ]

    The step that catches the worst mistake is verifying the bundle, because a service_role key in the shipped app is a full compromise that no Row Level Security policy can contain.

    Scan your app for the service_role key

    Because a service_role key in the client defeats every policy you set, confirming what keys actually ship is a necessary companion to writing good Row Level Security.

    A scanner like PTKD.com analyzes your app build and reports issues such as leaked keys and secrets, insecure data storage, and over-broad permissions by severity, mapped to OWASP MASVS, so a service_role key that reached the client is flagged, while the anon key is expected. To be clear about the boundary: PTKD does not write your Row Level Security policies, which you manage in Supabase, and it does not enable them for you. It confirms your shipped app does not carry the secret key that would bypass the protection your policies provide.

    What to take away

    • The anon key is public and constrained by Row Level Security, so it belongs in the frontend; the service_role key bypasses Row Level Security and must stay on a server.
    • The anon key goes in the frontend always, and the service_role key never ships in your app, because it ignores the policies that make the anon key safe.
    • The anon key is safe in the client only when Row Level Security is enabled and correct on every table it reaches, since it is powerless beyond what policies allow.
    • The service_role key grants full access no policy limits, so an exposed one is a full compromise and must be rotated immediately, unlike the anon key.
    • Verify that only the anon key ships and Row Level Security is on, and scan your app with a tool like PTKD.com to catch a leaked service_role key.
    • #supabase
    • #anon key
    • #service role key
    • #row level security
    • #api keys

    Frequently asked questions

    Which Supabase key goes in the frontend?
    The anon key, always, and the service_role key never. Supabase apps query the database directly from the client using the anon, or publishable, key, which is designed to be public and shipped in your app, so including it in your web or mobile frontend is the intended setup. The service_role key is a secret for trusted server environments only; putting it in your frontend hands anyone who inspects your app full control of your database.
    What is the difference between the anon and service_role keys?
    The anon key is public and carries no privileges on its own, so every request with it is checked against your Row Level Security policies and can only do what they allow. The service_role key is a secret that bypasses Row Level Security entirely and has full read and write access to all your data. So your policies contain the anon key, while the service_role key walks straight through them, which is why one ships to clients and the other stays server-side.
    Does the service_role key bypass RLS?
    Yes, completely, and that is its defining property. Where the anon key is checked against your policies on every request, the service_role key skips those checks and has full access, by design so trusted server code can perform privileged operations. Because it ignores Row Level Security, no policy protects you if it is exposed: an attacker with it can read, modify, or delete anything regardless of your policies, so it must live only on a server you control.
    Is the anon key safe to expose?
    Yes, when Row Level Security is enabled and correct, because the anon key is public by design and its safety comes entirely from your policies. It is powerless beyond what you allow, so exposing it is expected. The critical dependency is Row Level Security: a table with it disabled or a policy granting everyone access is fully open to the anon key. Most Supabase incidents come from missing policies, not the public key being visible.
    What do I do if I exposed the service_role key?
    Treat it as a full compromise and rotate it immediately, because it grants complete access no policy limits. In your Supabase dashboard, roll the service_role key so the exposed value stops working, then update your server-side code and remove the key from wherever it leaked, moving its operations to a server. Review your database and logs for misuse. The anon key, by contrast, does not need rotating on exposure since Row Level Security keeps it safe.
    How do I confirm no service_role key shipped in my app?
    Audit the shipped build, since a service_role key in the client defeats every policy you set. A scanner like PTKD.com (https://ptkd.com) analyzes your app build and reports leaked keys and secrets, insecure storage, and over-broad permissions by severity, mapped to OWASP MASVS, flagging a service_role key that reached the client while the anon key is expected. It does not write your Row Level Security policies, but it confirms no secret key bypasses the protection they provide.

    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