How to Deploy Netlify Securely (2026)

When Netlify builds your app, it clones your git repository and runs your build command. Any variable prefixed with VITE_, REACT_APP_, or NEXT_PUBLIC_ gets compiled into the JavaScript that ships to your visitors' browsers. That includes secrets you stored "safely" in Netlify's dashboard, because the prefix is an instruction to copy the value into the bundle regardless of where the variable came from.

In CheckYourVibe scans of Netlify-deployed apps, VITE_-prefixed secrets are the single most common exposure pattern, ahead of committed .env files and hardcoded strings.

TL;DR

Netlify's biggest security risk is the VITE_ (or REACT_APP_) prefix: it bakes secret keys into your client JavaScript at build time. Fix it by removing the prefix and moving the API call into a Netlify Function. Then add security headers via netlify.toml, protect deploy previews with access control, scope production keys to the Production context only, and scan the live URL before going public.

Step 1: Audit Your Environment Variables

Start by confirming what is actually in your JavaScript bundle.

1

Check the live bundle in DevTools. Open your deployed Netlify app, press F12, go to Sources, and search (Cmd/Ctrl+F) for your key prefix: sk_proj_ (OpenAI), sk_live_ (Stripe), AKIA (AWS), eyJhbGci (Supabase JWTs). If it appears in any .js file under your domain, it is in the client bundle.

2

Search your codebase for public-prefixed secrets.

bash
# Vite-based projects (React, Vue, Svelte)
grep -r "VITE_" . --include="*.env*" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.vue"

# Create React App
grep -r "REACT_APP_" . --include="*.env*" --include="*.ts" --include="*.tsx" --include="*.js"

# Next.js on Netlify
grep -r "NEXT_PUBLIC_" . --include="*.env*" --include="*.ts" --include="*.tsx" --include="*.js"

Any variable named VITE_OPENAI_API_KEY, REACT_APP_STRIPE_SECRET, or NEXT_PUBLIC_ANTHROPIC_KEY is in your visitor's browser. The Netlify dashboard doesn't change this: the prefix instructs the build tool to embed the value regardless of where it came from.

3

Check your git history for committed .env files.

bash
git log --all --full-history -- .env
git log --all --full-history -- .env.local
git log --all --full-history -- .env.production

If these return any commits, those secrets were readable during every Netlify CI build that included them. Anyone who cloned the repo has a copy.

The Netlify dashboard shows you which variables are set, not which ones are safe. A variable stored in the dashboard with a VITE_ prefix is just as exposed as one hardcoded in source code.

Step 2: Move Secret API Calls to Netlify Functions

Netlify Functions run in a Node.js environment on your domain. They read environment variables at runtime via process.env, never at build time, so nothing ends up in the client bundle.

Create a netlify/functions/ directory in your repo root. Netlify deploys everything in it automatically.

netlify/functions/chat.ts
import { Handler } from "@netlify/functions";
import OpenAI from "openai";

// process.env.OPENAI_API_KEY (no VITE_ prefix)
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

export const handler: Handler = async (event) => {
  if (event.httpMethod !== "POST") {
    return { statusCode: 405, body: "Method Not Allowed" };
  }

  const { message } = JSON.parse(event.body || "{}");
  if (!message) {
    return { statusCode: 400, body: "message required" };
  }

  const response = await client.chat.completions.create({
    model: "gpt-4o",
    messages: [{ role: "user", content: message }],
  });

  return {
    statusCode: 200,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ reply: response.choices[0].message.content }),
  };
};

Your React component calls /.netlify/functions/chat on your own domain. The function calls OpenAI. The key never reaches the browser.

In the Netlify dashboard, rename VITE_OPENAI_API_KEY to OPENAI_API_KEY (no prefix). The variable now scopes to Functions only.

Old (exposed)New (safe)
VITE_OPENAI_API_KEYOPENAI_API_KEY
REACT_APP_STRIPE_SECRETSTRIPE_SECRET_KEY
VITE_ANTHROPIC_API_KEYANTHROPIC_API_KEY
VITE_SUPABASE_SERVICE_ROLE_KEYSUPABASE_SERVICE_ROLE_KEY

The Supabase anon key is designed to be public and safe with VITE_ as long as you have Row Level Security enabled on every table. The service_role key bypasses RLS entirely and must never reach the browser.

Step 3: Set Security Headers via netlify.toml

Netlify does not add security headers by default. Without them, your site ships without clickjacking protection, content type sniffing prevention, or HSTS.

Add or update netlify.toml in your repository root:

netlify.toml
[build]
  command = "npm run build"   # or bun run build, vite build, etc.
  publish = "dist"            # or build/, public/, .next/

[[headers]]
  for = "/*"
  [headers.values]
    X-Frame-Options = "DENY"
    X-Content-Type-Options = "nosniff"
    Referrer-Policy = "strict-origin-when-cross-origin"
    Permissions-Policy = "camera=(), microphone=(), geolocation=()"
    Strict-Transport-Security = "max-age=31536000; includeSubDomains"
    Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https:;"

The CSP above is a starting baseline. Tighten script-src by removing 'unsafe-inline' if your app doesn't use inline scripts, and restrict connect-src to specific domains your app actually calls.

After deploying with the updated netlify.toml, verify the headers are live: curl -I https://yourdomain.netlify.app and check that x-frame-options, x-content-type-options, and strict-transport-security appear in the response. Netlify strips custom headers for local netlify dev sessions, so always test against the live deployment.

Step 4: Protect Deploy Previews

Every pull request generates a public Netlify preview URL. Any VITE_-prefixed variable in that build is visible at that URL. By default, these URLs have no access control.

Go to Site Configuration > Access Control and enable one of:

  • Site password: visitors enter a password before seeing the preview
  • Netlify Identity: visitors must sign in with a Netlify Identity account

For most teams, a site password on deploy previews is the simplest option. It prevents the preview URL from being a live demo of your app using production credentials.

Once you've protected previews, also set preview-specific environment variables. In Site Configuration > Environment Variables, scope your secret keys:

VariableProductionDeploy PreviewsBranch deploys
OPENAI_API_KEYsk-prod-real-keysk-test-safe-keysk-test-safe-key
STRIPE_SECRET_KEYsk_live_...sk_test_...sk_test_...
DATABASE_URLprod DBstaging DBstaging DB

A leaked preview URL then exposes test credentials, not production ones.

Step 5: Add a Pre-commit Hook

This prevents the VITE_ leak from recurring.

bash
# Install gitleaks
brew install gitleaks   # macOS
# or: pip install gitleaks, or download from github.com/gitleaks/gitleaks/releases

# Test current repo
gitleaks detect --source . --verbose

# Add pre-commit hook
cat > .git/hooks/pre-commit << 'HOOKEOF'
#!/bin/sh
gitleaks protect --staged --verbose
if [ $? -ne 0 ]; then
  echo "gitleaks found secrets in staged files. Commit blocked."
  exit 1
fi
HOOKEOF
chmod +x .git/hooks/pre-commit

Add .env, .env.local, and .env.production to your .gitignore if they aren't already:

bash
echo ".env" >> .gitignore
echo ".env*.local" >> .gitignore
echo ".env.production" >> .gitignore
git add .gitignore && git commit -m "chore: gitignore .env files"

Step 6: Scan Before Going Live

Before you remove the under-construction page or announce your launch, run a CheckYourVibe scan on the live Netlify URL.

The scanner checks:

  • JavaScript bundle for embedded API keys (OpenAI, Stripe, Supabase, AWS, and 30+ others)
  • HTTP response headers against the OWASP recommended set
  • Supabase service_role key used client-side
  • CORS policy set to * on your Netlify Functions
  • Mixed content and insecure resource loading

It takes about 90 seconds and gives you a prioritized list. Fix the criticals before launch, not after your first real users arrive.

If your Netlify app is still behind a maintenance page or access control, you can scan the deploy preview URL directly. CheckYourVibe follows authentication headers you provide.

Are Netlify environment variables safe?

Variables stored in Netlify's dashboard without a public prefix are encrypted and only available in Netlify Functions at runtime. They're never compiled into the client bundle. The prefix is what makes them unsafe: VITE_MY_SECRET=value tells Vite to embed value into the output JavaScript regardless of where the variable came from.

What's the difference between VITE_ and process.env in Netlify?

VITE_ variables are substituted at build time: Vite replaces every reference to import.meta.env.VITE_FOO with the raw string value in the compiled JavaScript. process.env variables inside a Netlify Function are resolved at runtime in a Node.js environment on Netlify's servers. The value never touches the browser. Secret keys must use process.env inside a Function, never VITE_.

Do Netlify deploy previews expose my environment variables?

Yes, if those variables have a VITE_ or REACT_APP_ prefix. Every pull request triggers a public preview build that includes your environment variables. Without preview access control, anyone with the URL can open DevTools and read those values. Scope production keys to the Production context and use test credentials for previews.

How do I add security headers to a Netlify site?

Add a [[headers]] block to netlify.toml targeting /*. Headers defined there apply to every response from your site. You can also create a _headers file in your publish directory using the same header name/value syntax. netlify.toml takes precedence when both files exist.

What should I scan for before launching on Netlify?

Check your live JavaScript bundle for exposed API keys (any VITE_-prefixed secret), confirm security headers appear in actual HTTP responses, verify deploy previews require authentication, and test that your Netlify Functions return 401 for unauthenticated requests on any endpoint that reads or writes your database.

CheckYourVibe scans your live Netlify deployment for exposed API keys in the JavaScript bundle, missing security headers, and Supabase service_role misuse. Free scan, no signup required.

How-To Guides

How to Deploy Netlify Securely (2026)