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
NEXT_PUBLIC_SUPABASE_URL=https://...- Available EVERYWHERE including browser
- Bundled into client JavaScript
- Anyone can see these
VERCEL_ENV=production- Set by Vercel automatically
- Used for environment detection
Security Rules
Rule 1: Never NEXT_PUBLIC_ sensitive data
// 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:
// See all public environment variables
Object.keys(window.__NEXT_DATA__.runtimeConfig || {})Setting Variables in Vercel
- Go to Project Settings → Environment Variables
- Add each variable
- Select environments (Production, Preview, Development)
- Check "Sensitive" for secrets (hides from logs)
Security Headers
Configure in next.config.js:
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:
{
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
// 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
// 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
// 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
// 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 definedPREVIEW 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
// 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
// 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.