How to Hide API Keys in a Lovable, Bolt or Cursor App
By ExcenityAI · Published · Updated
To hide an API key, keep it out of any variable your framework sends to the browser, store it as a server-side secret, and call the paid service from a server or edge function that your frontend calls instead. In Lovable and Bolt that means a Supabase Edge Function with the key in Secrets; in Cursor projects it usually means a server route with the key in an environment variable. If a key already reached the browser or GitHub, rotate it first.
Why do keys leak from AI-built apps?
Frontend build tools bundle some variables into the JavaScript that every visitor downloads. Vite's documentation says variables prefixed with VITE_ are exposed in client-side source code after bundling and should not contain sensitive information such as API keys. Next.js does the same for NEXT_PUBLIC_: the value is inlined into the JavaScript sent to the browser at build time.
So the rule is simple. A prefix that exposes a variable to the browser is for public values only. Anything that can spend money or read private data belongs on a server. We cover how to find leaked keys in the security guide, and why the Supabase anon key is a different case in the RLS guide.
What is the pattern for hiding a key?
- Store the key as a secret on the server side, never with a
VITE_orNEXT_PUBLIC_prefix. - Write a server function that reads the secret and calls the paid service.
- Have your frontend call your function, not the paid service.
- In that function, check the user is logged in and rate-limit it, otherwise anyone can use your key through your endpoint.
Hiding the key only helps if the function is not an open proxy. The last step matters as much as the first.
How do you do it in Lovable, Bolt or Cursor?
Lovable
- Open More, then Settings, then Secrets, and click Add secret. Lovable's docs say secrets are encrypted, injected into Edge Functions at runtime and never reach the browser.
- Lovable also opens a secure input in chat when a feature needs a key, and saves a key you paste into chat as a project secret. Prefer the secure input.
- Secrets are write-only, so you can replace or delete a value but not read it back. Lovable rejects
VITE_names in Secrets, because those values are public by design. - If you use an external Supabase project, the Manage secrets button opens your Supabase dashboard instead.
- Do not remove
.envfrom git in Lovable. The docs say it must stay committed for builds, which is another reason it should only hold public values such as the Supabase URL and publishable key.
Bolt
- Click the database icon at the top centre of the project, then Secrets. Enter a name and value and click Save.
- Bolt's docs say server functions, sometimes called edge functions, read these secrets, so the values stay out of users' browsers. Bolt can also prompt you for a secret when you ask for an integration such as OpenAI.
- On individual accounts only the project owner can manage secrets. On team accounts that is owners, co-owners and admins.
- If you deploy a Bolt project to Netlify instead, set the key in Netlify's environment variables and read it in a Netlify function. Choosing the Functions scope is a Pro and Enterprise feature; on other plans variables apply to all scopes. Netlify's docs say variables set only in
netlify.tomlare not available to functions, and a new deploy is needed for changed values to apply.
Cursor and any other stack
Cursor edits ordinary code, so the pattern depends on your stack. In Next.js, variables without NEXT_PUBLIC_ are only available on the server, including in route handlers:
// app/api/summarize/route.ts
export async function POST(req: Request) {
// TODO: verify the user's session and rate-limit here
const { text } = await req.json();
const res = await fetch("https://api.example.com/v1/summarize", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.EXAMPLE_API_KEY}` },
body: JSON.stringify({ text }),
});
if (!res.ok) return Response.json({ error: "Upstream request failed" }, { status: 502 });
return Response.json(await res.json());
}Set EXAMPLE_API_KEY in your host's environment variables, never in a committed file, and redeploy after changing it.
How do you set up a Supabase Edge Function secret?
If you manage Supabase yourself, set the secret from the dashboard's Edge Function Secrets page or with the CLI, then read it in the function with Deno.env.get.
supabase secrets set EXAMPLE_API_KEY=your-key-here
supabase functions deploy summarize- Secret names cannot start with
SUPABASE_, which is reserved. - Deployed functions pick up new secrets immediately, with no redeploy.
- From the frontend, call
supabase.functions.invoke('summarize', { body: { text } }). - Keep local secrets in
supabase/functions/.envand add.envfiles to.gitignore.
How do you rotate a leaked key?
Rotate before you clean anything else. GitHub's guidance on removing sensitive data starts with revoking or rotating the secret, because once rotated it can no longer be used for access.
- Create a new key in the provider's dashboard and add it to your secrets.
- Redeploy or invoke the app and confirm it works with the new key.
- Revoke the old key in the provider's dashboard.
- Check the provider's usage page for calls you did not make, and set a spend limit. See how to cap your API bills.
Lovable has a Rotate action for its own built-in AI key, which it replaces in your Edge Function environment automatically. For every other provider, rotate in the provider's dashboard.
How do you purge a key from git history?
Deleting the file in a new commit does not remove the key from earlier commits. After rotating, rewrite history with git-filter-repo, as GitHub's guide describes. The --sensitive-data-removal flag needs git-filter-repo 2.47 or later:
git clone https://github.com/YOUR-USERNAME/YOUR-REPOSITORY
cd YOUR-REPOSITORY
git-filter-repo --sensitive-data-removal --invert-paths --path .env
git push --force --mirror origin- To replace text instead of deleting a file, use
--replace-textwith a file listing the strings. - GitHub warns the data can stay reachable in forks and through cached views by commit hash, and that a collaborator pushing from an old clone can bring it back. GitHub Support can remove cached views and pull-request references.
- Collaborators should rebase, not merge, any branches made from the old history.
Because of those leftovers, a rewritten history is not a substitute for rotation.
How do you stop it happening again?
GitHub push protection blocks a push that contains a supported secret before it reaches the repository. GitHub's docs describe it as blocking contributors from pushing secrets and alerting you when someone bypasses the block.
- Open the repository, then Settings.
- Under Security and quality, click Advanced Security.
- Enable Secret Protection if it is not already on, then enable Push protection.
A contributor can bypass a block by choosing a reason (used in tests, false positive or fix later) when secret scanning is enabled, so treat any bypass as a prompt to rotate. Then work through the security checklist to check the rest.
Frequently asked questions
Is a key safe if I delete it from my repository?
No. It stays in earlier commits, and GitHub warns it can remain reachable in forks and cached views. Rotate the key first, then rewrite history if you want to clean up.
Can I put a secret in a .env file if the repository is private?
Do not commit it. Keep secrets in your host's environment variables or secrets store and add `.env` files to `.gitignore`. The exception is Lovable, whose docs say `.env` must stay committed, so it should hold only public values.
Why does my app break after I rename a variable from VITE_ to a secret?
Browser code can no longer read it, which is the point. Move the call that used it into a server or edge function, and have the frontend call that function.
Will GitHub push protection catch every key?
No. It detects supported secret patterns, so a custom or unusual token can slip through. Treat it as a safety net behind keeping keys out of code in the first place.
Why is my function still using the old value after I changed it?
On Netlify, values are applied at deploy time, so you need a new deploy for a changed variable to take effect. Supabase Edge Function secrets are different: deployed functions pick up new secrets immediately.
Sources
Checked 31 Oct 2026. Provider settings and limits change, so confirm against the linked documentation.
- Vite: env variables and modes
- Next.js: environment variables
- Lovable: Secrets
- Bolt: Secrets
- Supabase: Edge Function secrets
- Netlify: environment variables and functions
- GitHub: removing sensitive data from a repository
- GitHub: push protection from the command line (bypass)
- GitHub: enabling push protection for your repository