Skip to content
StackSiren

How should Bolt apps set environment variables on Netlify?

By StackSiren · Published

Netlify environment variables that a Vite build reads under a VITE_ prefix are embedded into public JavaScript. Put secrets in unprefixed Netlify variables and read them only from Netlify Functions (or other server code) — never from the Bolt browser bundle.

Bolt (bolt.new) projects are often Vite + React and frequently deploy to Netlify. The platform's env UI is easy to misread: "available at build time" sounds private. For Vite it is not. Same rule as Are VITE_ environment variables secret?. Also check the live site for a public .env and keep Netlify security headers in place.

Build vs runtime vars on Netlify

Netlify documents variables that are:

  • Used while the site builds (Vite, bundlers, static generation)
  • Available to Functions / Edge Functions at runtime
  • Optionally scoped to Deploy contexts (production, deploy previews, branch deploys)

For a classic Vite SPA:

Kind Example names Ends up in browser?
Public build-time VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY Yes — inlined into JS
Secret runtime OPENAI_API_KEY, STRIPE_SECRET_KEY, SUPABASE_SERVICE_ROLE_KEY No, if only Functions read them
Accidental secret VITE_OPENAI_API_KEY Yes — treat as leaked

Set secrets in Site configuration → Environment variables without the VITE_ prefix, and enable them for the contexts that run Functions. Public Supabase anon/publishable values may use VITE_ on purpose — they are designed to be public, but only with correct RLS (anon key safety).

The VITE_ trap

Vite's docs are explicit: only env vars prefixed with VITE_ are exposed to client code, via import.meta.env.VITE_…. Anything else is available to Node during the build but not automatically shipped to the browser — unless you hand it to a plugin or inline it yourself.

Dangerous pattern in a Bolt/Vite app:

// UNSAFE — ships in the browser bundle after Netlify build
const openai = import.meta.env.VITE_OPENAI_API_KEY;

Safer split:

// Browser — public identifiers only
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnon = import.meta.env.VITE_SUPABASE_ANON_KEY;
// netlify/functions/chat.js — secret stays on the server
export async function handler(event) {
  const key = process.env.OPENAI_API_KEY;
  if (!key) return { statusCode: 500, body: "missing key" };
  // call OpenAI with key; never return the key to the client
}

After deploy, search the built JS:

curl -fsS https://your-bolt-site.netlify.app/assets/*.js 2>/dev/null | head -c 0
# Or open DevTools → Sources and search for sk- / sk_live_ / service_role
grep -R "sk-" dist/assets || true

If a secret-shaped string appears in dist/, rotate it and remove the VITE_ prefix. Guides: exposed API keys, exposed env files.

Netlify Functions for secrets

  1. Create netlify/functions/… (or the Functions directory your Bolt export uses).
  2. Set OPENAI_API_KEY (etc.) in the Netlify UI or netlify.toml [build.environment] without VITE_. Prefer the UI / secret store for production values so they are not committed.
  3. Read with process.env.NAME inside the Function only.
  4. Call the Function from the browser (/.netlify/functions/chat) with the user's session or your own auth check — do not pass the secret back.
  5. Restrict deploy-preview access to secrets when previews are public: use context-scoped env vars so preview deploys get stub keys or none at all.

Bolt-specific UI labels change; if the in-editor "secrets" panel syncs to Netlify, still verify the deployed bundle and Function logs. Do not assume a green checkbox means the value stayed server-side.

Example: OpenAI from a Bolt UI without leaking the key

Browser form posts to your Function; the Function holds the key:

<!-- simplified -->
<form id="ask">
  <textarea name="prompt"></textarea>
  <button type="submit">Ask</button>
</form>
<script type="module">
  document.getElementById('ask').onsubmit = async (e) => {
    e.preventDefault();
    const prompt = new FormData(e.target).get('prompt');
    const res = await fetch('/.netlify/functions/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ prompt }),
    });
    const data = await res.json();
    console.log(data.reply);
  };
</script>
// netlify/functions/chat.js
export async function handler(event) {
  if (event.httpMethod !== 'POST') {
    return { statusCode: 405, body: 'POST only' };
  }
  const { prompt } = JSON.parse(event.body || '{}');
  const key = process.env.OPENAI_API_KEY;
  if (!key) return { statusCode: 500, body: 'server misconfigured' };

  const r = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${key}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: 'gpt-4o-mini',
      messages: [{ role: 'user', content: String(prompt || '').slice(0, 4000) }],
    }),
  });
  const json = await r.json();
  const reply = json.choices?.[0]?.message?.content ?? '';
  return {
    statusCode: 200,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ reply }),
  };
}

Add auth (session cookie, Supabase JWT check, or Netlify Identity) before you expose a paid API behind a public Function. Rate-limit if the endpoint is anonymous. The important part for this article: OPENAI_API_KEY never appears in dist/.

Checklist for a Bolt → Netlify ship

  1. Inventory every import.meta.env.VITE_* usage — public only.
  2. Move provider secrets to Functions (or Supabase Edge Functions / other backend).
  3. Confirm Netlify publish directory is the Vite dist (no .env / .git in the web root).
  4. Curl /.env and /.git/config on the production hostname.
  5. Add headers via _headers / netlify.toml (Netlify security headers).
  6. Re-scan after each deploy with StackSiren's free scan or the env file / API key checkers.

Deploy contexts (previews)

Netlify lets you scope variables to production vs deploy previews vs branch deploys. Use that:

  • Production: real Stripe/OpenAI/service_role secrets for Functions only.
  • Deploy previews: stub keys, or omit secrets so preview Functions fail closed rather than billing you for public preview abuse.
  • Never put production service_role keys in a context that untrusted collaborators can trigger.

If a Bolt preview URL is world-readable, treat every VITE_ value and every Function secret attached to that context as potentially exposed to anyone who opens the preview.

FAQ

Can I use Netlify env vars without VITE_ in browser code? Not through import.meta.env. Vite will not expose them. You need a Function or other server path.

Is VITE_SUPABASE_ANON_KEY OK? Usually yes — the anon/publishable key is public by design. Security still depends on RLS and grants.

Do build-time secrets in netlify.toml stay private? Values in git-committed netlify.toml are visible to anyone with repo access. Prefer the Netlify UI / sealed secrets for production keys. Build-time injection into client bundles is still a leak if prefixed VITE_.

What about NEXT_PUBLIC_ if I exported to Next? Same class of problem as VITE_: public by design. See Is NEXT_PUBLIC safe?.

Bolt changed its env UI — is this still accurate? The Vite bundling rule is framework-level and stable. Always verify with a Sources search on the live Netlify hostname after deploy.

Check your app now

Free passive scan, results in under a minute.

Run a free scan

Fix guides for this topic

Platform checklists: Bolt

Related posts

Sources