All articles
Deployment11 min readJanuary 15, 2026
VercelNext.jsProductionEnvironment Variables

Vercel Deployment Security: Protecting Your Next.js App in Production

Complete guide to securing Next.js applications on Vercel. Environment variables, edge functions, headers, and production hardening.

Security Guide

Vercel + Next.js: Security Done Right

Vercel makes deployment trivially easy. That simplicity can mask important security decisions. This guide covers everything you need to secure your Next.js app on Vercel.

Environment Variables

The most critical security decision on Vercel.

The Three Types

1. Plain Environment Variables

DATABASE_URL=postgres://...
  • Available at build time and runtime
  • Baked into serverless functions
  • NOT available to client-side code
2. NEXT_PUBLIC_ Variables
NEXT_PUBLIC_SUPABASE_URL=https://...
  • Available EVERYWHERE including browser
  • Bundled into client JavaScript
  • Anyone can see these
3. Sensitive Variables (System Environment Variables)
VERCEL_ENV=production
  • Set by Vercel automatically
  • Used for environment detection

Security Rules

Rule 1: Never NEXT_PUBLIC_ sensitive data

javascript
// WRONG - Exposed to all users
NEXT_PUBLIC_DATABASE_URL=postgres://user:password@host/db
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_...

// RIGHT - Server-side only DATABASE_URL=postgres://user:password@host/db STRIPE_SECRET_KEY=sk_live_...

Rule 2: Use different values per environment

Production:  STRIPE_SECRET_KEY=sk_live_...
Preview:     STRIPE_SECRET_KEY=sk_test_...
Development: STRIPE_SECRET_KEY=sk_test_...

Rule 3: Audit what's exposed

In your browser console:

javascript
// See all public environment variables
Object.keys(window.__NEXT_DATA__.runtimeConfig || {})

Setting Variables in Vercel

  1. Go to Project Settings → Environment Variables
  2. Add each variable
  3. Select environments (Production, Preview, Development)
  4. Check "Sensitive" for secrets (hides from logs)

Security Headers

Configure in next.config.js:

javascript
const securityHeaders = [
  {
    key: 'X-DNS-Prefetch-Control',
    value: 'on'
  },
  {
    key: 'Strict-Transport-Security',
    value: 'max-age=63072000; includeSubDomains; preload'
  },
  {
    key: 'X-XSS-Protection',
    value: '1; mode=block'
  },
  {
    key: 'X-Frame-Options',
    value: 'SAMEORIGIN'
  },
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff'
  },
  {
    key: 'Referrer-Policy',
    value: 'strict-origin-when-cross-origin'
  },
  {
    key: 'Permissions-Policy',
    value: 'camera=(), microphone=(), geolocation=()'
  }
]

module.exports = { async headers() { return [ { source: '/:path*', headers: securityHeaders, }, ] }, }

Content Security Policy

For Next.js apps:

javascript
{
  key: 'Content-Security-Policy',
  value: 
    default-src 'self';
    script-src 'self' 'unsafe-eval' 'unsafe-inline';
    style-src 'self' 'unsafe-inline';
    img-src 'self' data: https:;
    font-src 'self';
    connect-src 'self' https://api.yourservice.com;
  .replace(/\s{2,}/g, ' ').trim()
}

Preview Deployments

Every PR gets a preview URL. This can expose:

  • Development features
  • Debug information
  • Incomplete security

Protecting Previews

Option 1: Vercel Password Protection

Project Settings → Password Protection → Enable

Option 2: Authentication Check

javascript
// middleware.ts
import { NextResponse } from 'next/server'

export function middleware(request) { // Allow production if (process.env.VERCEL_ENV === 'production') { return NextResponse.next() }

// Require auth on previews const authHeader = request.headers.get('authorization') if (!authHeader || !isValidAuth(authHeader)) { return new NextResponse('Unauthorized', { status: 401, headers: { 'WWW-Authenticate': 'Basic' } }) } }

Option 3: Vercel Authentication (Teams)

Enterprise feature for team-level access control.

Edge Functions Security

Secure Patterns

javascript
// middleware.ts
import { NextResponse } from 'next/server'

export function middleware(request) { // Rate limiting at the edge const ip = request.ip ?? '127.0.0.1' const rateLimit = checkRateLimit(ip)

if (rateLimit.exceeded) { return new NextResponse('Too Many Requests', { status: 429 }) }

// Block suspicious patterns const url = request.nextUrl.pathname if (url.includes('..') || url.includes('\0')) { return new NextResponse('Bad Request', { status: 400 }) }

return NextResponse.next() }

Edge Function Environment Variables

Edge functions have access to environment variables, but:

  • They run in a different runtime (Edge Runtime)
  • Some Node.js APIs aren't available
  • Be careful with large secrets (bundle size)

API Route Security

Authentication Middleware

javascript
// lib/auth.ts
import { getServerSession } from 'next-auth'

export async function requireAuth(request) { const session = await getServerSession()

if (!session) { throw new Error('Unauthorized') }

return session }

// app/api/data/route.ts import { requireAuth } from '@/lib/auth'

export async function GET(request) { try { const session = await requireAuth(request) // Handle authenticated request } catch { return Response.json({ error: 'Unauthorized' }, { status: 401 }) } }

CORS Configuration

javascript
// app/api/public/route.ts
export async function GET(request) {
  const origin = request.headers.get('origin')
  const allowedOrigins = ['https://yourapp.com', 'https://app.yourapp.com']

if (!allowedOrigins.includes(origin)) { return Response.json({ error: 'Forbidden' }, { status: 403 }) }

return Response.json(data, { headers: { 'Access-Control-Allow-Origin': origin, 'Access-Control-Allow-Methods': 'GET, POST', } }) }

Vercel Security Checklist

PRE-DEPLOYMENT
==============
[ ] All secrets in Vercel Environment Variables
[ ] No NEXT_PUBLIC_ for sensitive data
[ ] Different values for production vs preview
[ ] Security headers configured
[ ] CSP policy defined

PREVIEW DEPLOYMENTS =================== [ ] Password protection enabled OR [ ] Auth middleware for previews [ ] No production secrets in preview

API ROUTES ========== [ ] Authentication required where needed [ ] CORS restricted to known origins [ ] Rate limiting implemented [ ] Input validation on all endpoints

MONITORING ========== [ ] Vercel Analytics enabled [ ] Error tracking (Sentry) configured [ ] Log drains set up (if needed)

Common Mistakes

1. Exposing database URL

javascript
// WRONG
NEXT_PUBLIC_DATABASE_URL=...

// RIGHT DATABASE_URL=... // (no NEXT_PUBLIC_)

2. Same secrets everywhere

// WRONG - Same key in all environments
STRIPE_KEY=sk_live_xxx (used in preview too)

// RIGHT - Test key for preview Production: STRIPE_KEY=sk_live_xxx Preview: STRIPE_KEY=sk_test_xxx

3. Missing middleware

javascript
// WRONG - No protection
export async function GET() {
  return Response.json(await db.query('SELECT * FROM users'))
}

// RIGHT - With auth export async function GET() { const session = await requireAuth() return Response.json(await getUserData(session.userId)) }

The Bottom Line

Vercel handles infrastructure security. You handle application security. Configure environment variables correctly, add security headers, protect previews, and authenticate your APIs.

Easy deployment doesn't mean easy security—but it can be straightforward.

Ready to secure your AI-generated code?

Stop reading about vulnerabilities. Start fixing them.

Start Scanning Free