---
title: "Public env vars, the Supabase anon key, and row level security"
slug: ai-builder-supabase-env-and-rls
severity: high
category: Secrets
summary: "Vite bundles a VITE_ value into client source, and Next.js inlines NEXT_PUBLIC_ at build time. A Supabase publishable key in those names is public by design. Row level security is what limits the rows that key can touch."
platforms:
  - Bolt
  - Vite
  - Next.js
  - Supabase
  - v0
updated: "2026-10-16"
published: "2026-10-16"
related:
  - secret-keys-in-the-frontend
  - supabase-tables-without-rls
  - supabase-data-api-grants
  - bolt-rls-keys-and-auth
  - v0-keys-rls-and-auth
securityClaim: true
---

Two public prefixes put an environment variable in the browser. This page is that pattern in two cases: a Bolt project that uses Vite, and a v0 project that uses Next.js. It is not a count of Bolt or v0 apps, and it is not a claim about every app.

[Vite's env guide](https://vite.dev/guide/env-and-mode) says a variable prefixed with `VITE_` is exposed in client source after bundling, and that `VITE_*` variables should not contain sensitive information such as API keys, because those values are bundled into source at build time. A name without that prefix is not exposed that way. The guide's example is `VITE_SOME_KEY` visible through `import.meta.env`, and `DB_PASSWORD` undefined there.

[Vercel's v0 docs](https://vercel.com/docs/v0) say v0 works with tools like Next.js. [Next.js environment variables](https://nextjs.org/docs/app/guides/environment-variables) say that, by default, a variable without the `NEXT_PUBLIC_` prefix is only available in the Node.js environment, and that prefixing a variable with `NEXT_PUBLIC_` inlines the value into the JavaScript sent to the browser when you run `next build`. The [`env` option in next.config](https://nextjs.org/docs/app/api-reference/config/next-config-js/env) is a separate path: a name listed there is included in the JavaScript bundle even without `NEXT_PUBLIC_`.

[Supabase API keys](https://supabase.com/docs/guides/getting-started/api-keys) say the publishable key is what you ship in a browser, and that anyone can read it, so it only reaches what row level security allows. With nobody signed in, that key is the `anon` role. With a signed-in user, it is `authenticated`. The secret key is `service_role`. It bypasses row level security, and it must not go in a browser, a shipped app, or source control. The legacy `anon` key is the older publishable key. The legacy `service_role` key is the older secret key. Supabase says it is deprecating both legacy keys by the end of 2026.

Postgres checks grants before row level security. The same guide says a missing grant returns a permission error, including for `service_role`, and a policy that matches no rows returns an empty result. [Securing your API](https://supabase.com/docs/guides/api/securing-your-api) says a table or view exposed through the Data API without row level security can be accessed by any role with a matching grant. A missing grant on a new table is the separate failure in [New Supabase tables need an explicit grant](/issues/supabase-data-api-grants).

Keep the two rules apart.

Safe to put in `VITE_` or `NEXT_PUBLIC_`: the project URL and the publishable key. Supabase documents that key as safe to expose in a web page, and it only reaches what row level security allows. That holds when row level security is enabled and the policies for `anon` and `authenticated` are the ones you mean. Vite's warning about API keys still applies to a secret key, a `service_role` key, or any other credential. The Vite page and the Supabase page do not collapse into one rule.

Not safe to expose: a secret key or a `service_role` key in `VITE_` or `NEXT_PUBLIC_`. The API keys guide says that key bypasses row level security. Next.js inlines `NEXT_PUBLIC_` into the browser bundle at `next build`. Vite bundles `VITE_` into client source.

What row level security has to enforce: policies for `anon` and `authenticated`. The API keys guide says policies never apply to a secret key, because `service_role` has `BYPASSRLS`. A publishable key in the client is not a substitute for those policies.

## How to check in 5 minutes

1. In a Bolt project that uses Vite, search for `VITE_` and for `import.meta.env`. Vite's guide says a `VITE_` value is in the client bundle and a name without that prefix is not. A secret key, `service_role`, or `sb_secret_` under `VITE_` is the failure. The project URL and the publishable key are the names that can use the prefix, and only with row level security on.
2. In a v0 project that uses Next.js, search for `NEXT_PUBLIC_` and for names under `env` in `next.config`. By default, Next.js inlines a `NEXT_PUBLIC_` value at `next build`, and a variable without that prefix stays in the Node.js environment. The `env` option includes a name in the browser bundle without that prefix. Confirm the secret key is in neither place.
3. In the Supabase dashboard, confirm row level security is enabled on tables the browser client queries, and read the policies for `anon` and `authenticated`. The API keys guide says to enable row level security on every table before you deploy, and to review those policies. Send one signed-out request with the publishable key on a project you own. Another user's row is the failure. An empty result is a policy that matched no rows. `permission denied for table` is a missing grant, covered in [New Supabase tables need an explicit grant](/issues/supabase-data-api-grants).

## How to fix

### Bolt

Bolt projects that use Vite follow the `VITE_` rule above. Remove the prefix from any secret, and stop reading that name from `import.meta.env` in client code. Keep the Supabase project URL and publishable key in `VITE_` names only after row level security is on and you have read the policies. Do not put the secret key in a `VITE_` variable.

### Vite

Treat every `VITE_` value as client source. Vite's guide says the prefix exists so the value is bundled, and that it should not hold an API key. A name without the prefix is undefined on `import.meta.env`. Move a secret to a server or edge function. The Supabase publishable key is the exception Supabase documents, and only with row level security enabled.

### v0

On a v0 project that uses Next.js, the public prefix is `NEXT_PUBLIC_`, not `VITE_`. Keep the project URL and publishable key in `NEXT_PUBLIC_` names. Keep the secret key in a server module that client components do not import, under a name without that prefix. Build again after you change the variable, because Next.js inlines `NEXT_PUBLIC_` at `next build`.

### Next.js

By default, Next.js exposes a browser value only when the name starts with `NEXT_PUBLIC_`. A name without that prefix stays in the Node.js environment, so server code can read `process.env.SUPABASE_SECRET_KEY`. The `env` option in `next.config` is separate: Next.js says a name listed there is included in the JavaScript bundle even without `NEXT_PUBLIC_`. Do not put a secret key in either place.

### Supabase

Enable row level security, then add policies for the roles the browser uses. The API keys guide says to write those policies for `anon` and `authenticated`, and that policies do not apply to a secret key. Grant only the commands each role needs. A table with a grant and row level security off is reachable by that role, which Securing your API states. The publishable key in the client does not replace the policies.

## Agent prompt

```text
You are checking public environment variables in a Bolt, Vite, v0, or Next.js project that uses Supabase. Do not add features. Do not invent a count of insecure apps.

1. List every VITE_ and NEXT_PUBLIC_ name, and every name under env in next.config. Vite bundles VITE_ into client source. By default Next.js inlines NEXT_PUBLIC_ at next build. A name in next.config env is also in the browser bundle. A Vite name without the VITE_ prefix is not exposed that way.
2. The Supabase project URL and publishable key may use that prefix. The secret key and the legacy service_role key may not. Supabase says the publishable key is public and only reaches what row level security allows. The secret key bypasses row level security.
3. Enable row level security and show the policies for anon and authenticated. Do not turn row level security off. Do not grant every command to anon to hide a permission error.
4. A missing grant is a permission error. A policy that matches no rows returns an empty result. Do not treat that empty result as a missing grant.
5. Do not print secret values. Show the files you changed and the env var names the host needs.
```

## FAQ

### The anon key is in my JavaScript. Is that a leak?

The legacy `anon` key and the publishable key are what Supabase says you ship in the browser. They are not a secret by themselves. They are a problem when row level security is off, or when a policy lets `anon` or `authenticated` read rows you meant to keep private, because the key only reaches what those policies allow.

### Vite says not to put API keys in VITE_. Can the Supabase publishable key use that prefix?

Yes, and the two warnings are different. Vite says `VITE_*` values are bundled into client source and should not contain sensitive information such as API keys. Supabase says the publishable key is safe to expose because it only reaches what row level security allows, and that a secret key must not go in a browser. Put the publishable key in `VITE_` or `NEXT_PUBLIC_`. Do not put the secret key there.

### If row level security is on, can I put the secret key in NEXT_PUBLIC_ or VITE_?

No. The API keys guide says a secret key bypasses row level security, and that policies never apply to `service_role`. Next.js will still inline `NEXT_PUBLIC_`. Vite will still bundle `VITE_`. Row level security does not protect a query made with that key.

## Sources

- [Vite env variables and modes](https://vite.dev/guide/env-and-mode)
- [Next.js environment variables](https://nextjs.org/docs/app/guides/environment-variables)
- [Next.js next.config env option](https://nextjs.org/docs/app/api-reference/config/next-config-js/env)
- [v0](https://vercel.com/docs/v0)
- [Supabase API keys](https://supabase.com/docs/guides/getting-started/api-keys)
- [Securing your API](https://supabase.com/docs/guides/api/securing-your-api)
