Bolt shows your Supabase URL and API key. Is that a leak?
· PlugLeak
Seeing a Supabase URL and API key in a Bolt app's network tab is often normal. Here is how to tell a publishable anon key from a service_role secret, and what to do in each case.
Open a Bolt app, then DevTools, then the network tab. You will often see a request to something.supabase.co with the project URL and an API key in the headers, and the table name in the path. People building on Bolt ask whether that means the project has to be thrown away. Sometimes it is an emergency. Often it is the key Supabase tells you to ship.
The difference is which key it is. This page is how to classify the value in the request, what a visible URL actually exposes, and what to rotate only when the value is a secret. It assumes the app is already deployed. If you already know the key's role claim is service_role, use the service_role fix guide for the removal steps and come back here only for the rotation details.
What the network tab is showing
A Supabase client in the browser calls the Data API (PostgREST) on your project URL. The URL looks like https://<project-ref>.supabase.co. Supabase treats that host as a public identifier. Hiding it does not secure the database. Anyone who can load your JavaScript can read it, which is every visitor.
The request path is the API, not a leaked SQL file. A call that loads rows from tasks shows up as /rest/v1/tasks with the filters in the query string. That is the documented shape of the Data API. Seeing the table name does not mean the database password is in the page. The database password is a different secret, used by direct Postgres connections. It should never be in frontend code. Lovable, which uses Supabase the same way, stores third-party API keys for its edge functions in the Supabase project rather than in the app repository (Connect to Supabase). Bolt apps that talk to Supabase from React code are on the other side of that line: the browser has to hold a key, and that key has to be the low-privilege one.
Two headers matter. apikey is required on Data API requests (Securing your API). Authorization carries either that same public key, when nobody is signed in, or the user's access token after sign-in. Classify the apikey value. Do not panic at the user access token: it is that user's session, and it expires. Do panic if the apikey value is a secret key.
Four keys, two jobs
Supabase is replacing the old JWT keys. The docs say anon and service_role are deprecated by the end of 2026, in favor of publishable and secret keys (API keys). Both generations still show up in Bolt projects, and creating the new keys does not revoke the old ones. Read the string you found against this table. The privileges are Supabase's, not a guess.
sb_publishable_…Low privilege. Safe to put in a web page. Signed out, it acts as theanonrole. Signed in, the user's JWT selects theauthenticatedrole, and Row Level Security still applies.- Legacy anon key. A long JWT (three segments, starting with
eyJ) whose payloadroleisanon. Same job as the publishable key. Not a secret, and only safe together with RLS and least-privilege grants (Securing your data). sb_secret_…Elevated. Bypasses Row Level Security and can read and write project data. Servers, edge functions, and other backends only. Supabase says a secret key does not work in a browser: they match theUser-Agentand return HTTP 401. That block is not a reason to leave the key in the bundle. The same docs say an attacker can still use a leaked secret key from other tools, and you should rotate it.- Legacy
service_rolekey. A JWT whose payloadroleisservice_role. The old secret. It hasBYPASSRLS. There is no browser User-Agent block on this one. Anyone who copies it out of your JavaScript can call the API as the role that skips your policies.
Publishable and secret keys are short strings, not JWTs. Supabase notes that if a tutorial or an AI assistant tells you to paste a long eyJ key, it was written for the legacy keys. Bolt will happily follow that older instruction.
Classify the value without pasting it into a website
- If it starts with
sb_publishable_, it is the public key. Continue with the RLS checks below. You do not rebuild the app because this string is visible. - If it starts with
sb_secret_, treat it as leaked even if a browser request returns 401. Stop using that build in front of real users until you rotate, using the steps further down. - If it is a JWT, decode it on your own machine. The payload is the middle segment. It is base64url, not encryption. Anyone can read it, which is the point of checking.
node -e "console.log(Buffer.from(process.argv[1], 'base64url').toString())" 'MIDDLE_SEGMENT'You want a JSON object. The field that matters is role.
{ "iss": "supabase", "ref": "yourprojectref", "role": "anon" }anon is the public legacy key. service_role is the emergency. authenticated in that payload means you copied a user access token, not the project API key. Look at the apikey header again.
Then compare the value to Settings → API Keys in the Supabase dashboard. Match it there. Do not paste a secret key into a chat, an AI builder, or a hosted decoder. The dashboard already knows which key is which.
The free PlugLeak scan does this classification on the deployed site. It downloads the page and same-origin script files a visitor can already fetch, looks for JWTs, and reads the role claim. A service_role JWT is reported as critical. An sb_secret_ key is reported as critical. An anon JWT or an sb_publishable_ key is reported as informational, with a reminder that it is only safe when RLS is on. The scan does not query your database, does not try to log in, and does not prove your policies. A finding that names the anon key is not, by itself, a leak.
Why the key is in the bundle: the Vite prefix
Public Bolt threads about this scare describe React apps built with Vite. If that is your project, Vite's rule decides what is public. Variables prefixed with VITE_ are exposed in client source after the build. Variables without that prefix are not. Vite's docs say VITE_* values should not contain secrets such as API keys, because they are bundled into your source at build time (Env variables and modes).
# Shipped to every visitor. Fine for the project URL and the publishable key.
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=sb_publishable_...
# Stays off the client only if nothing in client code reads it.
# Never give the secret a VITE_ name.
SUPABASE_SECRET_KEY=sb_secret_...A .gitignore entry for .env stops git from committing the file. It does not stop Vite from inlining VITE_* into the JavaScript your host uploads. If a Bolt prompt "fixed" an undefined key by renaming SUPABASE_SECRET_KEY to VITE_SUPABASE_SECRET_KEY, the secret is in the bundle on purpose. Search the project for VITE_ and for service_role. Anything you would not paste in public does not get the prefix.
The same trap exists if the app is Next.js instead of Vite. Next.js inlines NEXT_PUBLIC_ into the browser bundle at build time, and leaves other variables on the server (Environment variables). Supabase's own env example uses NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY for the browser and an unprefixed SUPABASE_SECRET_KEY for the server, and says never to prefix the secret or the bundler will ship it.
After you remove the bad name, search the built output, not only the source. For a Vite app that is the dist directory. What you deploy is what the network tab shows.
If it is the publishable or anon key
Leave it in the client. Rebuilding the Bolt project to "hide" it does not change the security model. Supabase's instruction for frontend apps is to use the publishable key and turn on RLS with the least privilege the app needs. The key will still be visible afterward. That is the design.
What you do instead is confirm the key cannot read other people's rows. The Supabase RLS checklist is the SQL and the curl test for that. Short version, from those same docs: enable RLS on every table in an exposed schema, revoke grants the client roles should not have, and add policies so (select auth.uid()) = user_id on private tables. A policy of using (true) for anon makes the visible anon key as good as a dump of that table.
Test in a private window, signed out. If the screen shows another user's tasks, invoices, or emails, the key is "public by design" and the policies are wrong. Fix the policies. Do not rotate the anon key to compensate. Rotating the legacy JWT secret also invalidates the anon key and existing sessions, and Supabase now points people at the newer key types instead of that rotation (Rotating anon, service, and JWT secrets).
If it is a secret or service_role key
Assume someone else can use it. Supabase's rotation guide says to fix the cause of the leak before you rotate, or the new key leaks the same way (Rotate a leaked or compromised API key). Order:
- Remove the secret from client code and from any
VITE_orNEXT_PUBLIC_variable. Privileged work goes in a server you control, such as a Supabase Edge Function, reading the key from a server secret. The client keeps the publishable or anon key. - In the dashboard, Settings → API Keys, create a new secret key. Supabase's guide says you can also do this with the Management API. The dashboard path is enough.
- Point every backend, webhook, and edge function at the new key. If the leaked key was a legacy
service_roleJWT, also switch browser clients from the legacy anon key to a publishable key. Retiring the legacy key deactivates both legacy keys, anon and service_role together. - Delete the leaked secret key, or deactivate the legacy keys. Deleting a secret key cannot be undone. Supabase says not to re-enable legacy keys you already deactivated, because that restores the compromised value. Move any client you missed onto a new key instead.
- If the leaked key was a JWT-based
service_rolekey, follow the rest of that guide and revoke the legacy JWT secret. Anything that still trusts the old secret can still accept the leaked JWT. - Redeploy, then search the live JavaScript again. Old bundles and caches can keep the previous string. The PlugLeak scan is a check of what the live URL serves now, including script chunks linked from the page, not of git history.
The same pass looks for other shapes in that JavaScript: Stripe secret keys (sk_live_, rk_live_, sk_test_), OpenAI-style and Anthropic keys, AWS access key ids, GitHub tokens, and private-key blocks. It flags /.env or /.git/config only when the body looks like the real file. It does not open your database, and a postgres:// connection string without one of those shapes can slip through. Search the bundle for that yourself if you have ever put a connection string in client code.
What "ignore the .env" does not fix
Adding .env to .gitignore, or setting the variable in Netlify, Vercel, or Bolt's env UI, only affects the next build. It does not pull a key out of a file you already shipped. A public prefix still wins: the host passes VITE_SUPABASE_SECRET_KEY into the build, and Vite still embeds it. Use a name with no public prefix, and read it only from server code. If the Bolt app has no server, it cannot hold a secret. Put that operation in an edge function, or design it so the publishable key and RLS are enough.
Cursor projects have the same split. .cursorignore can keep .env out of agent and Tab context, and Cursor says terminal and MCP tools can still read ignored files (Ignore files). That is an editor control. Whatever the bundler inlines is what a visitor gets. Scan the URL you published.
A short decision
- Project URL in the network tab: expected.
- Table name in
/rest/v1/...: expected. sb_publishable_or a JWT withrole: anon: expected in the client. Safe only with grants and RLS. Use the RLS checklist.sb_secret_or a JWT withrole: service_role: not expected. Remove it from the client, rotate using the official steps, redeploy, and scan the live URL.- User access token in
Authorizationafter login: expected for that user. It is not the project secret. Do not log it.
If you are unsure which row you are in, paste the live URL into the free PlugLeak scan. It reports what a visitor can already download, including the Supabase role claim. It does not review RLS, storage policies, or business logic. A clear result is an automated check, not a security audit.
Sources
- Supabase: API keys
- Supabase: Securing your data
- Supabase: Securing your API
- Supabase: Migrating to publishable and secret keys
- Supabase: Rotate a leaked or compromised API key
- Supabase: Rotating anon, service, and JWT secrets
- Vite: Env variables and modes
- Next.js: Environment variables
- Lovable: Connect to Supabase
- Cursor: Ignore files
- r/boltnewbuilders: Bolt.new expose supabase url and api key
- r/boltnewbuilders: Exposed API key and URL of Supabase
Check your app now — free, passive, about 20 seconds.
Same scanner as the homepage scan. Paste the live URL. It reads what a visitor already downloads. It does not log in or query your database.