You have deployed to Vercel. It is live. But is it secure?
Vercel makes deployment effortless. Push to main, get a production URL in under a minute. That speed is genuinely impressive, but it also means security decisions that used to happen during a manual deployment process now happen (or don't happen) automatically. The defaults are reasonable, not bulletproof.
We scan thousands of Vercel-hosted Next.js apps at CheckVibe. The same security gaps appear repeatedly. Not because developers are careless, but because Vercel's deployment experience is so smooth that the security configuration step gets skipped entirely.
Here is the 10-point checklist we recommend for every Next.js app running on Vercel.
This is the single most common security mistake in Next.js applications, and Vercel's environment variable UI makes it easy to overlook. Any variable prefixed with NEXT_PUBLIC_ is bundled into client-side JavaScript and shipped to every visitor's browser.
Dangerous:
NEXT_PUBLIC_DATABASE_URL=postgresql://user:password@host/db
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_abc123...
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOi...
Every one of those values is now visible in the browser's developer tools. Anyone can extract them from your JavaScript bundle.
Correct approach:
# Client-safe (public keys only)
NEXT_PUBLIC_SUPABASE_URL=https://xyz.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOi...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
# Server-only (no NEXT_PUBLIC_ prefix)
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOi...
STRIPE_SECRET_KEY=sk_live_abc123...
DATABASE_URL=postgresql://user:password@host/db
Store server-only variables in the Vercel dashboard under Project Settings > Environment Variables. Do not commit them to .env files in your repository. Set the scope (Production, Preview, Development) appropriately so staging environments do not use production credentials.
Rule: If a key grants write access, admin privileges, or bypasses security policies, it must never carry the NEXT_PUBLIC_ prefix. For more on this pattern, see our guide on Next.js security best practices.
Vercel does not add security headers by default. Without explicit configuration, your Next.js app ships with no Content-Security-Policy, no HSTS preload, no clickjacking protection, and no referrer controls. You need to set these yourself.
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://*.supabase.co https://api.stripe.com",
"frame-ancestors 'none'",
].join('; '),
},
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains; preload',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
],
},
];
},
};
export default nextConfig;
Adjust the connect-src directive to include your actual API domains. If you use analytics or third-party scripts, add those origins to the appropriate directives. The goal is to be as restrictive as possible while keeping your app functional.
For a deep dive on each header, see our complete security headers guide.
Every pull request on Vercel generates a unique preview deployment URL. By default, these are publicly accessible. If your preview environments connect to production databases or contain unreleased features, anyone with the URL can access them.
Enable deployment protection:
Preview URLs follow a predictable pattern (project-name-git-branch-name-team.vercel.app), which means they are discoverable. Do not assume obscurity protects them.
Next.js Server Actions are convenient. They look like function calls. They are actually POST endpoints. Every Server Action is an HTTP endpoint that can be called directly with fetch or curl, bypassing your UI entirely.
Vulnerable:
// app/actions.ts
'use server';
export async function deleteUser(userId: string) {
await db.query('DELETE FROM users WHERE id = $1', [userId]);
}
Anyone can call this action with a forged request. There is no authentication, no authorization, and no input validation.
Fixed:
// app/actions.ts
'use server';
import { createClient } from '@/lib/supabase/server';
import { z } from 'zod';
const deleteUserSchema = z.object({
userId: z.string().uuid(),
});
export async function deleteUser(userId: string) {
const parsed = deleteUserSchema.safeParse({ userId });
if (!parsed.success) {
throw new Error('Invalid input');
}
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
throw new Error('Unauthorized');
}
// Check admin role
const { data: profile } = await supabase
.from('profiles')
.select('role')
.eq('id', user.id)
.single();
if (profile?.role !== 'admin') {
throw new Error('Forbidden');
}
await db.query('DELETE FROM users WHERE id = $1', [parsed.data.userId]);
}
Rule: Treat every Server Action exactly like an API route. Validate all inputs with a schema library. Check authentication and authorization on every call.
Every file in app/api/ is a publicly accessible HTTP endpoint. Next.js does not enforce any authentication by default. This means every route that reads or writes data must verify the caller's identity.
// app/api/projects/route.ts
import { createClient } from '@/lib/supabase/server';
export async function POST(request: Request) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
// Validate and sanitize input before processing
// ... your logic here
return Response.json({ success: true });
}
Do not rely on client-side routing to "hide" API routes. Bots and attackers will discover them through JavaScript bundle analysis, brute-force path scanning, or error messages that leak route structures.
Middleware in Next.js runs at the edge before any page or API route is reached. Use it to enforce authentication on protected routes, redirect unauthenticated users, and apply rate limiting.
// middleware.ts
import { createServerClient } from '@supabase/ssr';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
let response = NextResponse.next({
request: { headers: request.headers },
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => {
response.cookies.set(name, value, options);
});
},
},
}
);
const { data: { user } } = await supabase.auth.getUser();
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
const redirectUrl = request.nextUrl.clone();
redirectUrl.pathname = '/login';
return NextResponse.redirect(redirectUrl);
}
return response;
}
export const config = {
matcher: ['/dashboard/:path*', '/api/projects/:path*', '/api/keys/:path*'],
};
The matcher configuration is critical. Without it, middleware runs on every request including static assets, which hurts performance. Be explicit about which paths need protection.
Using Access-Control-Allow-Origin: * in production means any website on the internet can make authenticated requests to your API from a user's browser.
// app/api/data/route.ts
const ALLOWED_ORIGINS = [
'https://yourdomain.com',
'https://app.yourdomain.com',
];
export async function GET(request: Request) {
const origin = request.headers.get('origin') ?? '';
if (!ALLOWED_ORIGINS.includes(origin)) {
return new Response('Forbidden', { status: 403 });
}
const data = { /* ... */ };
return Response.json(data, {
headers: {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
Be explicit about which origins can access your API. If you have multiple subdomains, list them individually rather than using a broad regex that could match unintended domains. For background on CORS misconfigurations and their impact, see our CORS misconfiguration guide.
Vercel provides built-in DDoS protection and, on Pro and Enterprise plans, a Web Application Firewall (WAF). Understanding the boundary of these protections is important.
What Vercel handles automatically:
What Vercel does not handle for you:
Vercel's WAF rules (available on Pro plans) let you block traffic by IP, country, path pattern, and request header. Use them to restrict access to admin routes, block known bad IPs, and limit traffic to specific paths.
Do not assume the WAF replaces application-level security. It is a complementary layer, not a substitute for validating inputs and checking auth in your code.
By default, Next.js generates source maps in production builds. If these are exposed, attackers can read your original source code, understand your application logic, and find vulnerabilities more easily.
Disable source maps in production:
// next.config.ts
const nextConfig: NextConfig = {
productionBrowserSourceMaps: false,
// ... other config
};
In the Vercel dashboard, check Project Settings > Security for options related to:
Also verify that your error handling does not expose stack traces or internal paths to end users. Use generic error messages in production responses.
If your repository is public or accepts contributions from external forks, Vercel will build and deploy preview environments for forked pull requests by default. This means an attacker could submit a PR that modifies your build configuration, injects malicious code, or exfiltrates environment variables during the build step.
Mitigate this:
For open-source projects, this is especially important. A forked PR can run arbitrary code during npm run build, which has access to any environment variables scoped to preview deployments.
Beyond the checklist, these are the mistakes we see most often in production Vercel deployments.
Exposing secrets via NEXT_PUBLIC_ variables. This shows up in roughly 1 in 5 scans we run on Next.js apps. Developers copy environment variable configurations between projects and forget to remove the prefix from sensitive keys.
No Content-Security-Policy header. Without CSP, your app has no browser-enforced protection against XSS. If an attacker finds a way to inject a script tag, nothing stops it from executing.
Preview deployments accessible to the public. Preview URLs are not secret. They follow predictable naming patterns and are often indexed by search engines or shared in public Slack channels and GitHub comments.
Source maps shipped in production. These give attackers a complete map of your application code. Combined with other information, they make vulnerability discovery significantly faster.
Wildcard CORS in API routes. Using Access-Control-Allow-Origin: * on routes that return user data or accept mutations lets any website make requests on behalf of your users.
Missing auth on Server Actions. Because Server Actions look like regular function calls in your component code, it is easy to forget that they are exposed HTTP endpoints that anyone can invoke.
Vercel offers several security features beyond basic hosting. Not all are available on every plan, but they are worth knowing about.
Web Application Firewall (WAF): Available on Pro and Enterprise plans. Configure rules to block traffic by IP, geography, path, or header. Useful for restricting admin panels and blocking abusive traffic patterns.
DDoS Protection: Included on all plans. Vercel's edge network absorbs volumetric attacks automatically. For application-layer protection, you still need rate limiting in your code.
Deployment Protection: Password protection and Vercel Authentication for preview and staging environments. Essential for any project where preview deployments touch real data.
OIDC Federation: For Enterprise plans, Vercel supports OpenID Connect federation with identity providers. This lets you control who can deploy and access project settings through your existing SSO infrastructure.
Attack Challenge Mode: When under active attack, Vercel can automatically present challenge pages to suspicious traffic. This is managed through the dashboard or API.
Secure Compute: Enterprise feature that runs serverless functions in dedicated, isolated environments. Relevant for apps handling regulated data (healthcare, finance).
Note that Vercel Speed Insights and Web Analytics are performance tools, not security features. They measure Core Web Vitals and visitor metrics, but they do not detect or prevent attacks.
CheckVibe runs 36 automated security checks against any publicly accessible website, including Vercel-hosted Next.js apps. Our scanner suite includes a dedicated Vercel scanner that checks for platform-specific issues.
Here is what we detect on Vercel deployments:
Security headers analysis. We check for the presence and correct configuration of CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy. Missing or misconfigured headers are flagged with specific remediation steps.
Exposed secrets detection. Our API key leak scanner examines your client-side JavaScript bundle for patterns matching common API keys, database connection strings, and tokens. If a NEXT_PUBLIC_ variable contains something that looks like a secret key, we flag it.
CORS misconfiguration. We test your API endpoints for overly permissive CORS headers. Wildcard origins, missing credentials handling, and other CORS issues are identified and reported.
Technology fingerprinting. We identify that your app runs on Vercel and Next.js, then apply platform-specific security checks. The scanner knows which default configurations are insecure for your particular stack.
SSL and certificate analysis. We verify your TLS configuration, certificate chain, and HSTS setup. While Vercel handles certificates automatically, misconfigurations can still occur with custom domains.
Site crawling for comprehensive coverage. Our crawler discovers pages and API endpoints across your site, ensuring that security checks run against your entire attack surface, not just the homepage.
You can run a free scan on your Vercel deployment right now and see exactly which items from this checklist your app passes or fails.
Partially. Vercel handles infrastructure security: TLS certificates, DDoS protection, network isolation, and edge caching. It does not handle application security: authentication, authorization, input validation, security headers, CORS, or secret management. Those are your responsibility as the developer. Think of Vercel as a secure building — it has locks on the front door, but you still need to lock your apartment.
Not by default. Preview deployments are publicly accessible via predictable URLs. Enable Deployment Protection in your Vercel project settings to require authentication for preview URLs. Never scope production secrets (database credentials, API keys) to preview environments.
For most applications, Vercel's built-in DDoS protection combined with proper application-level security is sufficient. If you are handling sensitive data, processing payments, or facing targeted attacks, the Vercel WAF (Pro plan) adds useful IP-based and path-based blocking rules. It is not a replacement for input validation and auth checks in your code.
Configure them in next.config.ts using the headers() function as shown in checklist item #2. You can also add headers via vercel.json, but the next.config.ts approach is more maintainable and keeps your security configuration in version control alongside your application code.
If source maps are enabled in production (which is the default), attackers can reconstruct your original source code from the browser's developer tools. Set productionBrowserSourceMaps: false in next.config.ts. Additionally, enable Source Protection in Vercel's dashboard settings to prevent direct access to your deployment's source files.
Edge middleware is a good location for basic rate limiting because it runs before your application code. However, Vercel's edge runtime has limitations on execution time and available APIs. For sophisticated rate limiting (sliding windows, per-user limits, tiered thresholds), consider a dedicated rate limiting service or implement it in your API routes with a backing store like Redis.
Vercel is an excellent platform for deploying Next.js apps. But deploying is step one. Securing what you deployed is step two, and it does not happen automatically.
Work through this checklist from top to bottom. The first three items — environment variables, security headers, and deployment protection — will address the most common vulnerabilities we see. Then tackle auth, CORS, and the remaining items.
If you want to know exactly where your Vercel deployment stands right now, run a free CheckVibe scan. In under 60 seconds, you will get a full security report covering headers, exposed secrets, CORS, SSL, and 30+ additional checks tailored to your stack.
Paste your URL and get a security report in 30 seconds. 100+ automated checks with AI-powered fix prompts.
Scan your site freeRelated articles
The essential security checklist for SaaS founders shipping their first product. Covers auth, data protection, API security, payments, and monitoring — no security team needed.
The complete Supabase security checklist. Covers RLS, API keys, auth hardening, storage policies, edge functions, and more — with code examples and automated scanning.
Next.js apps are fast to build but easy to misconfigure. Here are 10 specific security issues most developers miss, with code examples for each vulnerability and its fix.