Vercel Deployment Security: Environment Variables, Preview Branches, and Headers
A security guide for Vercel deployments covering the dangers of NEXT_PUBLIC_ prefixed environment variables, encrypted env var management, securing preview branch deployments, and configuring security headers in next.config.ts.
Vercel makes deploying Next.js applications fast and straightforward. That ease of use, however, conceals several security pitfalls that catch teams when they have grown past a certain scale or when security requirements tighten. This guide covers the most important Vercel-specific security configurations: environment variable handling, preview deployment access controls, and security headers.
The NEXT_PUBLIC_ Danger
Next.js environment variables prefixed with NEXT_PUBLIC_ are inlined into the client-side JavaScript bundle at build time. This means they are visible to anyone who downloads and inspects your JavaScript — which any user of your application can do.
This is by design and is documented. The problem is that engineers sometimes incorrectly use NEXT_PUBLIC_ for values that should remain secret:
Never use NEXT_PUBLIC_ for:
- API keys that have write access (Stripe secret key, OpenAI API key, database credentials)
- Webhook signing secrets
- Internal service URLs that should not be publicly discoverable
- Admin tokens or service account credentials
- Encryption keys
Correct uses of NEXT_PUBLIC_:
- Analytics measurement IDs (Google Analytics G-XXXXXX, PostHog public token)
- Public Stripe publishable key (
pk_live_...orpk_test_...) - Feature flags that are already determined server-side
- Non-sensitive configuration (app name, support email, CDN URL)
The Stripe publishable key is a common point of confusion: NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is correct because the publishable key is designed to be public — it only allows creating payment intents. STRIPE_SECRET_KEY must never be prefixed with NEXT_PUBLIC_.
Audit your .env files:
# Find all NEXT_PUBLIC_ variables in your project
grep -r "NEXT_PUBLIC_" .env* --include="*.env*"
grep -r "NEXT_PUBLIC_" next.config.ts
# Check if any are used server-side only (a sign they might be mistakenly public)
grep -r "NEXT_PUBLIC_" app/api/ app/actions/ lib/
If you find a sensitive value exposed via NEXT_PUBLIC_, the remediation is:
- Remove the
NEXT_PUBLIC_prefix from the variable name - Update all server-side references to use the non-prefixed version
- Rotate the exposed secret immediately — assume it has been compromised
Environment Variable Management in Vercel
Vercel stores environment variables encrypted at rest and injects them at build time and runtime. However, there are important nuances:
Environment-scoped variables: In the Vercel dashboard, you can scope variables to Production, Preview, or Development. Use this to prevent production secrets from being available in preview deployments.
Best practice:
- Production secrets (database connection strings, payment API keys): Production only
- Preview secrets (staging database, test API keys): Preview only
- Development variables: Development only
Team secret management: For teams, sensitive values should be managed by a limited set of administrators. Vercel allows configuring environment variable access per team member role. Review who has "Read" and "Write" access to environment variables in your team settings.
The build log exposure risk: Vercel build logs are stored and accessible to all team members with access to the project. If any build step prints environment variables (e.g., console.log(process.env) during initialization, or a build script that echoes config), those values appear in the build logs. Audit your build scripts and next.config.ts initialization code for accidental variable logging.
Using Vercel's integration with secrets managers: For production deployments handling sensitive data, consider storing secrets in AWS Secrets Manager, HashiCorp Vault, or Doppler, and retrieving them at runtime rather than injecting them as build-time environment variables. This provides audit trails, rotation workflows, and access control beyond what Vercel's native environment variables offer.
// lib/secrets.ts — retrieve secrets at runtime from AWS Secrets Manager
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const client = new SecretsManagerClient({ region: 'us-east-1' });
export async function getSecret(secretId: string): Promise<string> {
const command = new GetSecretValueCommand({ SecretId: secretId });
const response = await client.send(command);
if (!response.SecretString) {
throw new Error(`Secret ${secretId} not found or empty`);
}
return response.SecretString;
}
Preview Deployment Security
Every PR and branch on Vercel automatically generates a preview deployment at a URL like https://my-app-git-feature-branch-org.vercel.app. These deployments:
- Are publicly accessible by default (anyone with the URL can access them)
- Use your production code (plus branch changes)
- May be configured with secrets that should not be public
The risks:
- Preview deployments may expose in-development features before they are ready for public disclosure
- If preview deployments use production or staging database connections, they may allow unauthorized data access
- Deployment URLs may be shared in Slack or GitHub comments, making them semi-public
Protection options:
Option 1: Password protection (Vercel Pro/Enterprise):
// vercel.json
{
"password": "your-preview-password"
}
This prompts for a password before serving any preview deployment. Appropriate for most teams.
Option 2: Vercel Authentication (Vercel Pro/Enterprise): Restrict preview deployments to Vercel team members only. Enable in Project Settings > Deployment Protection > Vercel Authentication.
Option 3: Custom domain with authentication middleware: If you need more granular control, deploy your preview URLs behind a Next.js middleware that checks for a shared secret in a cookie or URL parameter.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest): NextResponse {
// Only apply to preview deployments
const isPreview = process.env.VERCEL_ENV === 'preview';
if (!isPreview) {
return NextResponse.next();
}
// Check for preview access token
const previewToken = request.cookies.get('preview-token')?.value;
if (previewToken === process.env.PREVIEW_SECRET) {
return NextResponse.next();
}
// Check URL for token (for initial access via link)
const urlToken = request.nextUrl.searchParams.get('preview-token');
if (urlToken === process.env.PREVIEW_SECRET) {
const response = NextResponse.redirect(new URL(request.nextUrl.pathname, request.url));
response.cookies.set('preview-token', urlToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 3600
});
return response;
}
return new NextResponse('Preview access denied', { status: 401 });
}
Separate secrets for preview vs. production: Configure preview deployments with dedicated test API keys, a separate staging database, and preview-specific secrets. Never use production database credentials in preview deployments.
Security Headers in next.config.ts
Vercel applies response headers configured in next.config.ts consistently across all deployments. This is the recommended way to set security headers for Next.js on Vercel.
// next.config.ts
import type { NextConfig } from 'next';
const ContentSecurityPolicy = `
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://js.stripe.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: blob: https:;
font-src 'self' https://fonts.gstatic.com;
frame-src https://js.stripe.com https://hooks.stripe.com;
connect-src 'self' https://api.stripe.com https://www.google-analytics.com https://vitals.vercel-insights.com;
object-src 'none';
base-uri 'self';
form-action 'self' https://hooks.stripe.com;
frame-ancestors 'none';
upgrade-insecure-requests;
`;
const securityHeaders = [
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(), payment=()'
},
{
key: 'Content-Security-Policy',
value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()
}
];
const nextConfig: NextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: securityHeaders
}
];
},
// Additional security options
poweredByHeader: false, // Remove X-Powered-By: Next.js header
};
export default nextConfig;
Setting poweredByHeader: false removes the X-Powered-By: Next.js header, which fingerprints your technology stack for attackers.
Vercel-Specific CORS Configuration
Vercel's edge network handles OPTIONS preflight requests automatically in some configurations. When building APIs on Vercel, configure CORS explicitly in your route handlers:
// lib/cors.ts
import { NextRequest, NextResponse } from 'next/server';
const ALLOWED_ORIGINS = [
'https://app.example.com',
'https://www.example.com',
// Include preview URLs only in non-production environments
...(process.env.VERCEL_ENV !== 'production'
? ['https://*.vercel.app']
: [])
];
export function withCORS(response: NextResponse, request: NextRequest): NextResponse {
const origin = request.headers.get('origin');
if (origin && ALLOWED_ORIGINS.some(allowed =>
allowed.includes('*')
? new RegExp(allowed.replace('*', '.*')).test(origin)
: allowed === origin
)) {
response.headers.set('Access-Control-Allow-Origin', origin);
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
response.headers.set('Access-Control-Max-Age', '86400');
}
return response;
}
Rate limiting on Vercel: Vercel's firewall (Pro/Enterprise) supports rate limiting rules by IP, user agent, and custom headers. For API endpoints, configure rate limits in the Vercel dashboard to protect against abuse. For more granular application-level rate limiting, use Upstash Redis with @upstash/ratelimit.
Monitoring Vercel Deployments
Enable Vercel's audit log (Enterprise) or use the Vercel API to monitor:
- Who triggered deployments and when
- Environment variable changes
- Team member access changes
- Deployment failures
For security-relevant events, set up notifications via Vercel webhooks to your incident response system. A sudden spike in deployment failures or unauthorized environment variable changes may indicate a compromised CI/CD token.