Vercel Security: Env Vars, Preview Deploys, and Headers (2026 Guide)

On 19 April 2026, Vercel disclosed a security incident in which an attacker reached internal systems through a compromised third-party tool and, in Vercel's own words, "maneuvered through systems to enumerate and decrypt non-sensitive environment variables." Vercel told affected customers to treat those values, the API keys, tokens, database credentials and signing keys, as exposed and to rotate them as a priority.

Read the phrase "non-sensitive" again, because it is doing a lot of work. Vercel has a variable type called Sensitive, and the variables that came out of that incident were the ones nobody had marked with it.

That is one of the two ways credentials leave a Vercel project. The other one is entirely on your side of the line: the NEXT_PUBLIC_ prefix, which bundles a variable's value into the client-side JavaScript that every visitor downloads. Get that prefix wrong on a Stripe secret key and it is in the browser network tab of anyone who loads your site.

The fix is straightforward, but it requires auditing your .env file and your codebase. This guide covers that audit plus the six other Vercel security practices that matter most in 2026.

TL;DR

The fastest wins on Vercel security are: (1) auditing every NEXT_PUBLIC_ variable to confirm it holds no secret, (2) marking real secrets Sensitive so they cannot be read back out of the dashboard, which is exactly the control that separated the exposed variables from the safe ones in Vercel's April 2026 incident, (3) enabling Deployment Protection on preview deploys, and (4) adding security headers to vercel.json. All four take under 30 minutes combined.

"Vercel secures the infrastructure; you secure the application. Headers, environment variables, and middleware are your responsibility."

Best Practice 1: Add Security Headers 5 min

Security headers protect against common web attacks. Configure them in vercel.json:

vercel.json with security headers
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "X-Content-Type-Options",
          "value": "nosniff"
        },
        {
          "key": "X-Frame-Options",
          "value": "DENY"
        },
        {
          "key": "X-XSS-Protection",
          "value": "1; mode=block"
        },
        {
          "key": "Referrer-Policy",
          "value": "strict-origin-when-cross-origin"
        },
        {
          "key": "Permissions-Policy",
          "value": "camera=(), microphone=(), geolocation=()"
        }
      ]
    }
  ]
}

Content Security Policy

Add CSP for additional protection (customize based on your app's needs):

CSP header example
{
  "key": "Content-Security-Policy",
  "value": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.yourdomain.com"
}

Best Practice 2: Secure Environment Variables 3 min

Vercel has specific rules for environment variable exposure:

PrefixExposed to Browser?Use For
NEXT_PUBLIC_YesPublic API URLs, feature flags
No prefixNo (server only)API keys, secrets, credentials

Any environment variable starting with NEXT_PUBLIC_ is bundled into your client-side JavaScript. Anyone who visits your site can read it by opening DevTools. One misplaced prefix can expose a Stripe secret key, a Supabase service role key, or a database password to the entire internet.

NEXT_PUBLIC_ leak audit

Run this in your project root before every production deploy:

Audit NEXT_PUBLIC_ references in source
# Find every NEXT_PUBLIC_ variable used in your code
grep -r "NEXT_PUBLIC_" src/ --include="*.ts" --include="*.tsx" | grep "process.env"

# Then check each one in your .env file:
# - Should it ever be read on the server? Remove the prefix.
# - Is it a secret (key, token, password)? It must never have NEXT_PUBLIC_.
Correct environment variable usage
// Safe: Server-side only (no NEXT_PUBLIC_ prefix)
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
const databaseUrl = process.env.DATABASE_URL;

// Safe: Intentionally public
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const appUrl = process.env.NEXT_PUBLIC_APP_URL;

// WRONG: Secret with public prefix
// const apiSecret = process.env.NEXT_PUBLIC_API_SECRET; // Never do this!

Vercel secrets: mark them Sensitive

Getting the prefix right keeps a secret out of the browser bundle. It does nothing about who can read that secret inside your Vercel dashboard, and that is the gap the April 2026 incident walked through.

Vercel's own incident bulletin describes an attacker who compromised an employee account through a third-party tool, pivoted into a Vercel environment, and then "enumerated and decrypted non-sensitive environment variables." Read that phrase carefully. The variables that came out were the ones nobody had marked sensitive. Vercel's advice to affected customers included rotating those values and enabling sensitive environment variables going forward.

A Sensitive variable is one whose value is non-readable after you create it. Not hidden behind a click. Non-readable, by you too. You set it once, and if you ever need to know what it was, you rotate it instead of looking it up.

Marking a variable Sensitive is not a setting you can flip on an existing variable. Vercel's docs are explicit: to mark an existing environment variable as sensitive you have to remove it and re-add it with the option enabled. Every secret you created before today is still readable in your dashboard. Turning the feature on now protects the next variable, not the ones already sitting there.

Two limits worth knowing before you plan the migration:

  1. Production and Preview only. You cannot mark a Development-environment variable Sensitive. The toggle is disabled when Development is selected.
  2. Build-log redaction has a length floor. During builds, Vercel replaces a sensitive value with [REDACTED] if the value is 32 characters or longer. Shorter values are not redacted. A 20-character admin password or a short internal signing key is marked Sensitive, genuinely unreadable in the dashboard, and still printed in full if some build step echoes it. The two exceptions are VERCEL_AUTOMATION_BYPASS_SECRET and VERCEL_OIDC_TOKEN, which Vercel always redacts whatever their length.

So count the characters before you trust the redaction:

Check which of your secrets are under the redaction floor
# Pull your production env vars and flag any shorter than 32 chars.
vercel env pull .env.production --environment=production

awk -F= 'length($2) < 32 && $2 != "" && $0 !~ /^#/ {
  print length($2) "  " $1
}' .env.production | sort -n

rm .env.production   # do not leave this on disk

Anything that prints is a value Vercel will not mask in build logs. Either lengthen it (most tokens can be regenerated longer) or make sure no build step ever echoes it.

If you run a team, stop relying on people remembering the toggle. A team owner can open Settings > Security & Privacy > Environment Variable Policies and enable Enforce Sensitive Environment Variables, which makes every newly created Production and Preview variable sensitive by default. One switch. It does more for you than anything else in this guide, and we see the opposite constantly in scans: projects where the deploy pipeline is careful and the dashboard is still readable by every contractor who was ever added to the team.

Creating one from the API uses the sensitive type:

Create a sensitive environment variable via the Vercel API
curl --request POST \
  --url https://api.vercel.com/v10/projects/$PROJECT_ID/env \
  --header "Authorization: Bearer $VERCEL_TOKEN" \
  --header "Content-Type: application/json" \
  --data '[{
    "key": "STRIPE_SECRET_KEY",
    "value": "sk_live_...",
    "type": "sensitive",
    "target": ["production"]
  }]'

Best Practice 3: Protect Preview Deployments 2 min

By default, every Vercel preview deployment is publicly accessible via its generated URL. That URL is guessable by pattern and gets indexed by bots. Any feature branch you push (including one that contains a half-built auth bypass or a hardcoded test credential) is visible without authentication.

Protect them:

Vercel Authentication

  1. Go to Project Settings in Vercel Dashboard
  2. Navigate to Deployment Protection
  3. Enable Vercel Authentication for Preview deployments
  4. Configure which team members can access previews

Password Protection

For additional protection, add password authentication to previews:

Deployment Protection settings
// In Vercel Dashboard > Settings > Deployment Protection
{
  "protection": {
    "preview": {
      "enabled": true,
      "password": true  // Requires password for access
    }
  }
}

Best Practice 4: Use Edge Middleware for Auth 15 min

Protect routes at the edge before they reach your application:

middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Check for auth token
  const token = request.cookies.get('auth-token');

  // Protected routes
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }

  // Admin routes
  if (request.nextUrl.pathname.startsWith('/admin')) {
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
    // Additional admin check would go here
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*', '/api/protected/:path*']
};

Best Practice 5: Secure API Routes 15 min

Vercel API routes (or Next.js route handlers) need proper security:

Secure API route pattern
// app/api/user/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyAuth } from '@/lib/auth';

export async function GET(request: NextRequest) {
  // Verify authentication
  const authResult = await verifyAuth(request);
  if (!authResult.success) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }

  // Rate limiting check
  const rateLimitResult = await checkRateLimit(authResult.userId);
  if (!rateLimitResult.allowed) {
    return NextResponse.json(
      { error: 'Too many requests' },
      { status: 429 }
    );
  }

  // Proceed with authenticated request
  const userData = await getUserData(authResult.userId);
  return NextResponse.json(userData);
}

export async function POST(request: NextRequest) {
  const authResult = await verifyAuth(request);
  if (!authResult.success) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Validate input
  const body = await request.json();
  const validation = validateUserUpdate(body);
  if (!validation.success) {
    return NextResponse.json(
      { error: 'Invalid input', details: validation.errors },
      { status: 400 }
    );
  }

  // Process request...
}

Best Practice 6: Configure CORS Properly 10 min

For API routes that need CORS, configure it explicitly:

CORS configuration in API route
// app/api/public/route.ts
import { NextRequest, NextResponse } from 'next/server';

const allowedOrigins = [
  'https://yourdomain.com',
  'https://app.yourdomain.com',
];

export async function OPTIONS(request: NextRequest) {
  const origin = request.headers.get('origin');

  if (origin && allowedOrigins.includes(origin)) {
    return new NextResponse(null, {
      status: 204,
      headers: {
        'Access-Control-Allow-Origin': origin,
        'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
        'Access-Control-Max-Age': '86400',
      },
    });
  }

  return new NextResponse(null, { status: 403 });
}

export async function GET(request: NextRequest) {
  const origin = request.headers.get('origin');
  const response = NextResponse.json({ data: 'example' });

  if (origin && allowedOrigins.includes(origin)) {
    response.headers.set('Access-Control-Allow-Origin', origin);
  }

  return response;
}

Best Practice 7: Monitor and Alert 5 min

Set up monitoring for your Vercel deployment:

  • Vercel Analytics: Monitor performance and errors
  • Log Drains: Send logs to external monitoring services
  • Status Alerts: Get notified of deployment issues
  • Speed Insights: Track Core Web Vitals

Vercel Security Checklist:

  • Security headers configured in vercel.json
  • No secrets in NEXT_PUBLIC_ environment variables
  • Preview deployments protected with authentication
  • Edge Middleware protecting sensitive routes
  • API routes validate authentication and input
  • CORS configured with specific allowed origins
  • Monitoring and alerts enabled

Official Resources: For the latest information, see Vercel Security Documentation, Vercel Headers Configuration, and Deployment Protection Guide.

Is Vercel secure by default?

Vercel provides a secure infrastructure with automatic HTTPS, DDoS protection, and isolated builds. However, application-level security like headers, environment variable handling, and authentication is your responsibility to configure. The platform handles the server; what runs on it is up to you.

How do I audit my app for NEXT_PUBLIC_ leaks?

Run grep -r "NEXT_PUBLIC_" src/ --include="*.ts" --include="*.tsx" in your project root to find every variable your code exposes to the browser. Open your .env file side by side and verify none of those variables hold API keys, tokens, or passwords. If they do, remove the NEXT_PUBLIC_ prefix and move reads to a server component or API route.

Are environment variables safe on Vercel?

Server-side environment variables (without NEXT_PUBLIC_ prefix) are encrypted and only available during build and runtime on the server. Variables with NEXT_PUBLIC_ prefix are bundled into your client-side JavaScript, visible to any user who opens DevTools. The prefix is the only thing separating a safe public URL from an exposed secret key.

Should I protect preview deployments?

Yes. Every preview deployment is publicly accessible by default, indexed by bots, and shares your production environment variables unless you configure separate preview env vars. Enable Vercel Authentication under Project Settings > Deployment Protection so only logged-in team members can view preview URLs.

How do I protect API routes on Vercel?

Use Edge Middleware for route-level protection, implement authentication checks in each API route, add rate limiting, and validate all input. Never trust client-side data without server-side validation.

Further Reading

Put these practices into action with our step-by-step guides.

Verify Your Vercel Security

Scan your Vercel deployment for security headers and configuration issues.

Best Practices

Vercel Security: Env Vars, Preview Deploys, and Headers (2026 Guide)