You cannot hide the Supabase anon key in a React app, and you do not need to: the anon, or publishable, key is designed to be public and is safe in your frontend as long as Row Level Security is enabled and correct on every table. What must never appear in React is the service_role, or secret, key, because it bypasses Row Level Security and grants full access; that key belongs only on a server. So the real answer to hiding your key has two parts: make the public key safe with Row Level Security rules, and move any operation that needs the secret key behind a backend proxy, such as a Supabase Edge Function or your own server.
Short answer
Do not try to hide the anon key; secure it with Row Level Security and keep the secret key on a server. Per Supabase's API keys documentation, the anon or publishable key is meant to be used in client code and is safe to expose, while the service_role key bypasses Row Level Security and must stay server-side. Per Supabase's Row Level Security guide, policies decide what any request using the anon key can read or write, so they are your real protection. For anything requiring the secret key, use a backend proxy such as an Edge Function. If a secret key ever reached your frontend, rotate it immediately.
Why you cannot hide the anon key in React
A React app is client-side code that ships to the browser, so anything it contains, including your Supabase anon key, is visible to anyone who opens the developer tools or reads the bundle. There is no build trick, environment variable, or obfuscation that changes this, because the browser has to receive the key to use it. Variables prefixed for client exposure, such as those beginning with REACT_APP or similar in Vite, are inlined into the bundle precisely so the browser can read them, so they are not hidden at all.
This is why the goal of hiding the anon key is the wrong goal. Supabase designed the anon key to be public, the same way a website's URL is public, and your security does not depend on keeping it secret. Trying to conceal it wastes effort on something impossible and distracts from the control that actually matters, which is Row Level Security. Once you accept that the anon key is public by design, the question shifts from how to hide it to how to make it safe, and that question has a real answer.
Which key is which: safe versus unsafe
Supabase gives you two kinds of key, and only one belongs in React. The anon or publishable key is safe in the client because it carries no privileges on its own; every request it makes is subject to your Row Level Security policies, so it can only do what those policies allow. Including it in your React app is expected and supported, provided Row Level Security is doing its job.
The service_role or secret key is the opposite and must never appear in your frontend. It is designed to bypass Row Level Security entirely and has full read and write access to your data, so a copy of it in your React bundle hands anyone complete control of your database. The same applies to other server secrets such as your JWT secret or a direct database connection string. The rule is simple: the publishable key can live in React because policies constrain it, and the secret key cannot, because nothing constrains it.
Row Level Security rules: what actually protects your data
Row Level Security is the control that makes the public anon key safe, so it, not key hiding, is where your effort belongs. Row Level Security lets you write policies on each table that decide, per row, what a given request is allowed to read or write. With it enabled and policies in place, a request using the anon key can only reach the rows your policies permit, so even though the key is public, it is powerless beyond what you allow.
Write policies that tie access to the authenticated user. A common pattern is to compare a row's owner column against auth.uid(), the identifier of the signed-in user, so users can only see and change their own data. Enable Row Level Security on every table rather than assuming a default protects you, because a table without it, or with a policy that allows everything, is open to anyone with the public key. The frequent cause of Supabase data leaks is not an exposed anon key but a table where Row Level Security was disabled or a policy was written to permit all access. Getting these policies right is the actual security work.
The backend proxy: where the secret key belongs
For any operation that genuinely needs the service_role key or another server secret, the answer is a backend proxy rather than the React app. Instead of the browser calling Supabase with a privileged key, your React app calls a small server endpoint you control, that endpoint holds the secret key and performs the privileged operation, and it returns only the result. The secret never reaches the browser, so it cannot be extracted from your bundle.
With Supabase, the natural place for this is an Edge Function, which runs server-side and can safely use the service_role key for operations that must bypass Row Level Security, such as certain administrative tasks. Your own backend server works the same way. Protect the proxy so it is not an open door: require the user to be authenticated, check that they are allowed to perform the requested action, and scope each call to what that user should be able to do. The pattern is the same as any secret-handling design, keep the privileged key on the server and expose only a controlled endpoint to the client.
If you already exposed a secret key: rotate now
If a service_role key or other secret has already made it into your React app, treat it as compromised and rotate it immediately, because a key shipped to the browser is public and cannot be recalled. 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 re-architecting, since the old key remains dangerous for as long as it is valid.
After rotating, remove the secret from the frontend entirely and move its operations behind the backend proxy, because reissuing a key and putting the new one back into React simply exposes a fresh secret. Check your database and logs for any unexpected activity that suggests the exposed key was used, and review your Row Level Security policies while you are there, since an incident is a good prompt to confirm they are enabled and correct. Rotation stops the immediate bleed, and the architectural fix keeps the next key safe.
Verify your React app is safe
After securing the anon key with policies and moving secrets to a backend, verify rather than assume. Open your built React bundle and search it for any key beyond the publishable one, for the service_role key pattern, and for terms like secret or service_role, since a stray privileged key is the thing that must not be there. Confirm that only the anon or publishable key and your Supabase URL, both public by design, remain in the client.
Then test your Row Level Security, because a safe key with weak policies is still a leak. Using the anon key, try to read and write data you should not be able to, ideally as different users, and confirm the policies block what they should. Check that Row Level Security is enabled on every table and that no policy simply allows all access. The combination, only the public key in the bundle and correct policies behind it, is what makes your React app safe, and verifying both closes the loop.
Safe versus unsafe keys in React
Sorting your Supabase credentials tells you exactly what may ship in the client. The table below compares them.
| Credential | Put it in React? | Why |
|---|---|---|
| anon or publishable key | Yes, with Row Level Security | Public by design, limited by policies |
| service_role or secret key | Never | Bypasses Row Level Security, full access |
| JWT secret | Never | Signs tokens, server-side only |
| Database connection string | Never | Direct, unrestricted database access |
Read the table by what a credential can do alone: the publishable key is powerless without policies allowing access, so it can ship, while everything that carries privilege on its own must stay on a server.
Hardening checklist
Working through these steps secures the frontend and the data behind it. The checklist below covers them.
| Step | Action | Done? |
|---|---|---|
| Confirm key type | Only the anon or publishable key in the client | [ ] |
| Enable Row Level Security | On every table, denying by default | [ ] |
| Write policies | Scope rows to the authenticated user with auth.uid() | [ ] |
| Move secret operations | Behind an Edge Function or your server | [ ] |
| Rotate exposed secrets | Roll any service_role key that reached the client | [ ] |
| Verify | Inspect the bundle and test the policies | [ ] |
The step that carries the most weight is enabling and writing correct Row Level Security policies, because that, not hiding the key, is what actually protects your data.
Scan the build for leaked keys
Because a leaked secret is easy to miss, especially when an AI code generator wired up your Supabase client, checking what actually ships is worthwhile, particularly if your React app is also packaged as a mobile app through a wrapper or a native shell.
A scanner like PTKD.com analyzes your mobile 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 slipped into the shipped app is flagged before users can extract it. To be clear about the boundary: PTKD does not write your Row Level Security policies or build your backend proxy, and for a pure web bundle you inspect it directly. It finds a leaked key in the app you ship so you know what to rotate and remove.
What to take away
- You cannot hide the Supabase anon key in a React app, and you do not need to, because it is public by design and safe when Row Level Security is enabled and correct.
- The service_role or secret key must never appear in React; it bypasses Row Level Security and grants full access, so it belongs only on a server.
- Row Level Security policies, tying access to the authenticated user with auth.uid(), are the real protection, and most Supabase leaks come from disabled or all-permissive policies, not an exposed anon key.
- For operations needing the secret key, use a backend proxy such as a Supabase Edge Function, and if a secret ever reached the client, rotate it immediately.
- Verify only the public key ships and the policies hold, and scan the app you ship with a tool like PTKD.com to catch a leaked secret.



