Seeing NEXT_PUBLIC_SUPABASE_ANON_KEY in your client bundle is normal and, with Row Level Security enabled, safe. In Next.js, any environment variable prefixed with NEXT_PUBLIC_ is inlined into the browser bundle on purpose, and the Supabase anon key needs to be there so the browser client can connect. The anon key only grants the anonymous role, which your RLS policies constrain, so exposing it is by design. The genuine danger is different: never put your service_role secret key behind a NEXT_PUBLIC_ prefix, because that would expose the RLS-bypassing key to everyone. Check that only the anon key is public and that RLS is on.
Short answer
The exposed NEXT_PUBLIC_SUPABASE_ANON_KEY is expected, not a leak, as long as Row Level Security protects your data. Per Next.js environment-variable docs, the NEXT_PUBLIC_ prefix deliberately inlines a variable into the client bundle, and Supabase's browser client needs the anon key there. Per Supabase's API keys docs, the anon key only acts with the anonymous role, which your RLS policies restrict. So the anon key being visible is fine with RLS enabled. What must never be exposed is the service_role secret key, so make sure it has no NEXT_PUBLIC_ prefix and is used only in server code.
Why NEXT_PUBLIC_SUPABASE_ANON_KEY is in your client
The key is in your client because it has to be. In a Next.js app that talks to Supabase from the browser, the browser needs the anon key to authenticate its requests to your project, and the standard way to make a value available in the browser is to prefix its environment variable with NEXT_PUBLIC_. So NEXT_PUBLIC_SUPABASE_ANON_KEY appearing in the built client is the intended outcome of that setup, not an accident.
This trips people up because seeing a key in the shipped JavaScript feels like a mistake. It is worth internalizing that the anon key is a public identifier, not a secret. It tells Supabase which project a request belongs to and grants the anonymous role, and it is meant to sit in client code the same way a public API endpoint does. The real protection was never hiding this key; it is the policies behind it.
Is the anon key safe exposed like this?
Yes, with Row Level Security enabled and correct, it is safe for the anon key to be exposed in your client. Because the key only ever acts with the anonymous or authenticated role, and RLS constrains those roles to exactly what your policies allow, publishing it does not grant access to anything you have not permitted. This is the designed behavior of Supabase's client keys.
The safety is entirely conditional on RLS, though, so the question to ask is not whether the key is exposed but whether your policies are correct. If RLS is disabled, the exposed anon key becomes a key to everything the anonymous role can reach, which is all of it. So an exposed anon key with solid RLS is a non-issue, while the same key without RLS is a serious exposure. The key is only as safe as the policies behind it.
How NEXT_PUBLIC_ env vars work in Next.js
Next.js splits environment variables into two worlds based on the prefix. A variable with the NEXT_PUBLIC_ prefix is inlined into the client bundle at build time, so it is available in the browser and visible to anyone who inspects the shipped JavaScript. A variable without the prefix stays server-side, available only in server components, API routes, and other server code, and never sent to the browser.
This split is the mechanism you rely on to keep secrets safe. Anything that must reach the browser, like the Supabase URL and the anon key, gets the NEXT_PUBLIC_ prefix and is public by design. Anything that must stay secret gets no prefix and is used only on the server. Understanding this boundary is what lets you place each value correctly, because the prefix is literally the decision about whether a value is exposed.
The real danger: never NEXT_PUBLIC_ the service_role key
The dangerous mistake is prefixing the service_role secret key with NEXT_PUBLIC_. The service_role key, the sb_secret key in the newer format, bypasses Row Level Security entirely and has full access to your data, so it must never reach the browser. If you name it something like NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY, Next.js will faithfully inline it into the client bundle, exposing an all-powerful key to everyone.
Keep the service_role key as a plain, unprefixed environment variable, and use it only in server code, such as an API route or a server action. If you ever exposed it through a NEXT_PUBLIC_ prefix, treat it as a full compromise: rotate the key immediately and remove the prefix. The rule is simple and absolute: the anon key can be public, the service_role key never can, and the NEXT_PUBLIC_ prefix is what decides which happens.
RLS rules make the anon key safe
Row Level Security is the control that makes the exposed anon key acceptable, so it is where your effort belongs. Enable RLS on every table the client can reach, then write policies that grant each role only what it needs, for example letting users read and write only their own rows and giving the anonymous role access only to genuinely public data. With those in place, the exposed key is bounded by your policies.
Enable RLS first, because a table with RLS off is fully open to the anon key regardless of any policies. Then test your policies by querying as the anonymous role to confirm the restrictions actually hold, since a policy that is too permissive is nearly as bad as none at all. This verification is what turns an assumption that the exposed key is safe into a fact you have checked.
Which key goes where
Placing each Supabase value correctly in a Next.js app is straightforward once the rule is clear. The table below shows where each belongs.
| Value | Environment variable | Exposed to client? |
|---|---|---|
| Supabase URL | NEXT_PUBLIC_SUPABASE_URL | Yes, and that is fine |
| Anon or publishable key | NEXT_PUBLIC_SUPABASE_ANON_KEY | Yes, safe with RLS |
| Service_role or secret key | SUPABASE_SERVICE_ROLE_KEY, no prefix | No, server only |
| Any secret behind NEXT_PUBLIC_ | NEXT_PUBLIC_ prefixed secret | Yes, and that is a leak |
Read the table as a placement rule. The URL and anon key are meant to be public and get the prefix; the service_role key and any other secret get no prefix and stay on the server. A secret with a NEXT_PUBLIC_ prefix is always wrong.
Checklist
A short check confirms your setup is correct. The checklist below covers it.
| Check | Action | Done? |
|---|---|---|
| Confirm the public key | Verify only the anon key and URL are NEXT_PUBLIC_ | [ ] |
| No secret exposed | Ensure the service_role key has no NEXT_PUBLIC_ prefix | [ ] |
| RLS enabled | Turn on Row Level Security for every table | [ ] |
| Policies tested | Test policies as the anonymous role | [ ] |
| Scan the bundle | Confirm no secret key shipped in the client | [ ] |
The two that matter most are confirming the service_role key is never prefixed and that RLS is enabled and tested, because those decide whether your exposed anon key is safe and whether a truly dangerous key ever leaked. Everything else follows from getting those two right.
Verify with a scan
The anon key being public is expected, but it is worth verifying that no actual secret slipped into what you ship, since a misnamed variable or a copy-paste can put a service_role key into the client bundle. That is the exposure that matters, and it is easy to miss by eye in a large bundle.
A scanner like PTKD.com analyzes your app build and reports findings ordered by severity and mapped to OWASP MASVS, including secrets embedded in the client, so you can confirm that only the intended public key is present and no secret key leaked. To be clear about the boundary: PTKD does not configure your RLS or your Next.js environment variables. It verifies the shipped build, which complements the RLS work that keeps the exposed anon key safe.
What to take away
- NEXT_PUBLIC_SUPABASE_ANON_KEY appearing in your client bundle is expected; the NEXT_PUBLIC_ prefix inlines it on purpose, and the anon key is a public identifier.
- The anon key is safe to expose with Row Level Security enabled and correct, because it only grants the anonymous role, which RLS constrains.
- In Next.js, NEXT_PUBLIC_ variables reach the browser while unprefixed ones stay server-side; the prefix is the decision about exposure.
- Never prefix the service_role secret key with NEXT_PUBLIC_; keep it unprefixed and server-only, and rotate it if it was ever exposed.
- Enable and test RLS, confirm no secret is prefixed, and verify with PTKD.com that no secret key shipped in the client.




