TL;DR
The #1 Vercel security best practice is configuring security headers in vercel.json. These 7 practices take about 55 minutes to implement and address 81% of security issues found in Vercel deployments. Focus on: adding security headers, keeping secrets out of NEXT_PUBLIC_ variables, protecting preview deployments, and using Edge Middleware for authentication.
"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:
{
"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):
{
"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:
| Prefix | Exposed to Browser? | Use For |
|---|---|---|
| NEXT_PUBLIC_ | Yes | Public API URLs, feature flags |
| No prefix | No (server only) | API keys, secrets, credentials |
Important: Any environment variable starting with NEXT_PUBLIC_ will be included in your client-side bundle and visible to users. Never put secrets in NEXT_PUBLIC_ variables.
// 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!
Best Practice 3: Protect Preview Deployments 2 min
Preview deployments can expose unfinished features or test data. Protect them:
Vercel Authentication
- Go to Project Settings in Vercel Dashboard
- Navigate to Deployment Protection
- Enable Vercel Authentication for Preview deployments
- Configure which team members can access previews
Password Protection
For additional protection, add password authentication to previews:
// 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:
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:
// 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:
// 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.
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.
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 client code and visible to users.
Should I protect preview deployments?
Yes. Preview deployments can expose unfinished features, test data, or security vulnerabilities. Enable Vercel Authentication or password protection for all preview deployments, especially for production projects.
Verify Your Vercel Security
Scan your Vercel deployment for security headers and configuration issues.
Start Free Scan