Skip to content
SealMyApp

How to hide API keys in a Bolt or v0 app (and what VITE_ and NEXT_PUBLIC_ really mean)

VITE_ and NEXT_PUBLIC_ variables are copied into the JavaScript every visitor downloads. Keep secret keys in server-side code: a Next.js route handler in v0, a server or edge function in Bolt.

Updated 2026-10-16

You hide an API key by never sending it to the browser: store it as a server-side secret and call the API from a server route or function that your frontend calls instead. Any variable starting with VITE_ (Bolt’s usual Vite setup) or NEXT_PUBLIC_ (v0’s Next.js apps) is copied into the JavaScript every visitor downloads, so it is not hidden, whatever file it lives in.

That’s not a quirk of AI builders; it’s how the frameworks are designed. Vite’s env docs say VITE_ variables are exposed in client-side source after bundling and should not contain sensitive information such as API keys. Next.js’s environment variable docs say NEXT_PUBLIC_ values are inlined, at build time, into any JavaScript sent to the browser.

What the prefixes mean

VariableWhere it’s readableUse it for
VITE_ANYTHINGBrowser (bundled into your JS)Public values only: Supabase URL and publishable key, Stripe pk_ key
NEXT_PUBLIC_ANYTHINGBrowser (inlined at build time)Same as above
No prefix, e.g. OPENAI_API_KEYServer code only (Next.js route handlers and server code; Vite leaves it undefined in the browser)Secret keys

Removing the prefix isn’t enough on its own if the code that uses the key still runs in the browser: it will just get undefined. The call itself has to move to the server.

v0 (Next.js): use a route handler

1. Add the key without the prefix. v0’s external APIs docs say to open the project menu (...), then Settings → Environment Variables, and add e.g. OPENAI_API_KEY. Set it for Development too if you want it to work in v0’s preview, which v0’s docs say only reads Development variables.

2. Call the API from a route handler, which runs only on the server:

// app/api/ask/route.ts  (server only)
import { NextResponse } from "next/server";

export async function POST(req: Request) {
  // 1. Check the caller: only signed-in users (use your auth library here)
  const user = await getSignedInUser(req);
  if (!user) return NextResponse.json({ error: "Unauthorised" }, { status: 401 });

  // 2. Use the secret on the server
  const { prompt } = await req.json();
  const res = await fetch("https://api.provider.example/v1/...", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ prompt }),
  });
  return NextResponse.json(await res.json(), { status: res.status });
}
// In a client component: no key
const res = await fetch("/api/ask", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ prompt }),
});

Server components and server actions can also read unprefixed variables. Just never pass the key itself down to a client component as a prop.

Prompt for v0:

Move every call to <provider> out of client components into a route handler at
app/api/<name>/route.ts. Read the key from process.env.<SECRET_NAME> (no NEXT_PUBLIC_
prefix). Return 401 unless the caller is signed in. Update the client to call the
route. Remove any NEXT_PUBLIC_ variable that holds a secret.

Bolt (Vite + React): use a server function

A Bolt app is usually a Vite frontend, so there’s no server unless you add one. Bolt documents two routes:

  • Bolt Cloud: Bolt’s Secrets docs say server functions (also called edge functions) use secrets so sensitive values never reach users’ browsers. Add one via the database icon at the top centre, then Secrets, then Create secret. If you ask Bolt to integrate a service such as OpenAI, its docs say the agent will prompt you to add the secret.
  • Bolt with Supabase: put the key in your Supabase project’s Edge Function secrets and call the function with supabase.functions.invoke(). The function shape is the same as in our Lovable key guide.

Prompt for Bolt:

Remove <provider> API calls and keys from the frontend and from any VITE_
variable. Store the key as a secret named <SECRET_NAME> and create a server
function that reads it, checks the caller is signed in, and calls <provider>.
Call that function from the frontend. Show me every file you changed.

Don’t forget to lock the server route

A route that forwards anything to a paid API, with no check on who’s calling, is a leak in a different shape: anyone can use your key through it. At minimum:

  • Require a signed-in user (or a valid webhook signature, for webhooks).
  • Only accept the inputs you expect; don’t let the caller choose the URL or the model freely.
  • Add a per-user rate limit and set a spending cap at the provider if it offers one.

Then rotate anything that was already public

If a secret was ever in a VITE_ or NEXT_PUBLIC_ variable on a live site, assume someone has it. Create a new key at the provider, revoke the old one, and put only the new one in your server secrets. Then search your live JavaScript to confirm it’s gone: how to find leaked keys in your bundle.

FAQ

Does putting a key in .env hide it?

Only if the variable has no VITE_ or NEXT_PUBLIC_ prefix and only server code reads it. A .env file is just where the value is stored; the prefix and where you use it decide whether it ends up in the browser.

Which keys are fine to expose?

Keys designed for the browser: the Supabase anon or publishable key, the Firebase web config, Stripe pk_ publishable keys, and analytics IDs. Everything else (Stripe sk_/rk_, OpenAI, Anthropic, Resend, AWS, Supabase service_role/sb_secret_) belongs on the server. Check any key with our key checker.

Can I obfuscate or base64 the key instead?

No. The browser has to decode it to use it, so anyone can too, and it's visible in the Network tab when the request is sent.

I changed a NEXT_PUBLIC_ variable and nothing happened.

Next.js inlines NEXT_PUBLIC_ values at build time, so you have to redeploy. That's also why removing the prefix needs a rebuild before the old value disappears from your live JavaScript.

My key was already in a VITE_ or NEXT_PUBLIC_ variable in production.

Treat it as leaked: create a new key at the provider, revoke the old one, then move the call server-side as described here. See how to find leaked keys in your bundle.

Check it for free

Our free scan looks through your live app’s JavaScript for secret keys and checks your database and exposed files from the outside, after you verify you own the app. Run a free scan, or use the key checker to see whether a key is meant to be public. For the full list of pre-launch checks, see the vibe coding security checklist.

Sealed — a short weekly security note for vibe coders

One common security mistake in AI-built apps, how to fix it in your builder, and one useful tool. No spam; unsubscribe any time.