Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Expert knowledge for deploying to Vercel with Next.js
Capabilities
vercel
deployment
edge-functions
serverless
environment-variables
Prerequisites
Required skills: nextjs-app-router
Patterns
Environment Variables Setup
Properly configure environment variables for all environments
When to use: Setting up a new project on Vercel
// Three environments in Vercel:
// - Development (local)
// - Preview (PR deployments)
// - Production (main branch)
// In Vercel Dashboard:
// Settings → Environment Variables
// PUBLIC variables (exposed to browser)
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
// PRIVATE variables (server only)
SUPABASE_SERVICE_ROLE_KEY=eyJ... // Never NEXT_PUBLIC_!
DATABASE_URL=postgresql://...
// Per-environment values:
// Production: Real database, production API keys
// Preview: Staging database, test API keys
// Development: Local/dev values (also in .env.local)
Situation: Using NEXT_PUBLIC_ prefix for sensitive API keys
Symptoms:
Secrets visible in browser DevTools → Sources
Security audit finds exposed keys
Unexpected API access from unknown sources
Why this breaks:
Variables prefixed with NEXT_PUBLIC_ are inlined into the JavaScript
bundle at build time. Anyone can view them in browser DevTools.
This includes all your users and potential attackers.
Recommended fix:
Only use NEXT_PUBLIC_ for truly public values:
// SAFE to use NEXT_PUBLIC_
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... // Anon key is designed to be public
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
NEXT_PUBLIC_GA_ID=G-XXXXXXX
// NEVER use NEXT_PUBLIC_
SUPABASE_SERVICE_ROLE_KEY=eyJ... // Full database access!
STRIPE_SECRET_KEY=sk_live_... // Can charge cards!
DATABASE_URL=postgresql://... // Direct DB access!
JWT_SECRET=... // Can forge tokens!
// Access server-only vars in:
// - Server Components (app router)
// - API Routes
// - Server Actions ('use server')
// - getServerSideProps (pages router)
Preview deployments using production database
Severity: HIGH
Situation: Not configuring separate environment variables for preview
Symptoms:
Test data appearing in production
Production data corrupted after PR merge
Users seeing test accounts/content
Why this breaks:
Preview deployments run untested code. If they use production database,
a bug in a PR can corrupt production data. Also, testers might create
test data that shows up in production.
Recommended fix:
Set up separate databases for each environment:
// In Vercel Dashboard → Settings → Environment Variables
// Production (production env only):
DATABASE_URL=postgresql://prod-host/prod-db
Situation: API route or server component has slow initial load
Symptoms:
First request takes 3-10+ seconds
Subsequent requests are fast
Function size limit exceeded error
Deployment fails with size error
Why this breaks:
Vercel serverless functions have a 50MB limit (compressed).
Large functions mean slow cold starts (1-5+ seconds).
Heavy dependencies like puppeteer, sharp can cause this.
Recommended fix:
Reduce function size:
// 1. Use dynamic imports for heavy libs
export async function GET() {
const sharp = await import('sharp') // Only loads when needed
// ...
}
// 2. Move heavy processing to edge or external service
export const runtime = 'edge' // Much smaller, faster cold start
// 3. Check bundle size
// npx @next/bundle-analyzer
// Look for large dependencies
// 4. Use external services for heavy tasks
// - Image processing: Cloudinary, imgix
// - PDF generation: API service
// - Puppeteer: Browserless.io
// 5. Split into multiple functions
// /api/heavy-task/start - Queue the job
// /api/heavy-task/status - Check progress
Edge runtime missing Node.js APIs
Severity: HIGH
Situation: Using Node.js APIs in edge runtime functions
Symptoms:
X is not defined at runtime
Cannot find module fs
Works locally, fails deployed
Middleware crashes
Why this breaks:
Edge runtime runs on V8, not Node.js. Many Node APIs are missing:
fs, path, crypto (partial), child_process, and most native modules.
Your code will fail at runtime with "X is not defined".
// 2. Use streaming for long responses
export async function GET() {
const stream = new ReadableStream({
async start(controller) {
for (const chunk of generateChunks()) {
controller.enqueue(chunk)
await sleep(100) // Prevents timeout
}
controller.close()
}
})
return new Response(stream)
}
// 3. Use external services for heavy processing
// - Trigger serverless function, return job ID
// - Process in background (Inngest, Trigger.dev)
// - Client polls for completion
Environment variable missing at runtime but present at build
Severity: MEDIUM
Situation: Environment variable works in build but undefined at runtime
Symptoms:
Env var is undefined in production
Value doesn't change after updating in dashboard
Works in dev, wrong value in production
Requires redeploy to update value
Why this breaks:
Some env vars are only available at build time (hardcoded into bundle).
If you expect a runtime value but it was baked in at build, you get
the build-time value or undefined.
Recommended fix:
Understand when env vars are read:
// BUILD TIME (baked into bundle):
// - NEXT_PUBLIC_* variables
// - next.config.js
// - generateStaticParams
// - Static pages
// RUNTIME (read on each request):
// - Server Components (without cache)
// - API Routes
// - Server Actions
// - Middleware
// To force runtime reading:
export const dynamic = 'force-dynamic'
// For config that must be runtime:
// Don't use NEXT_PUBLIC_, read on server and pass to client
// Check which env vars you need:
// Build: URLs, public keys, feature flags (if static)
// Runtime: Secrets, database URLs, user-specific config
CORS errors calling API routes from different domain
Severity: MEDIUM
Situation: Frontend on different domain can't call API routes
Symptoms:
CORS policy error in browser console
No Access-Control-Allow-Origin header
Requests work in Postman but not browser
Works same-origin, fails cross-origin
Why this breaks:
By default, browsers block cross-origin requests. Vercel doesn't
automatically add CORS headers. If your frontend is on a different
domain (or localhost in dev), requests fail.
Recommended fix:
Add CORS headers to API routes:
// app/api/data/route.ts
export async function GET(request: Request) {
const data = await fetchData()
return Response.json(data, {
headers: {
'Access-Control-Allow-Origin': '*', // Or specific domain
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
// Handle preflight requests
export async function OPTIONS() {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
// Or use next.config.js for all routes:
module.exports = {
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: '*' },
],
},
]
},
}
Page shows stale data after deployment
Severity: MEDIUM
Situation: Updated data not appearing after new deployment
Symptoms:
Old content shows after deploy
Changes not visible immediately
Different users see different versions
Data updates but page doesn't
Why this breaks:
Vercel caches aggressively. Static pages are cached at the edge.
Even dynamic pages may be cached if not configured properly.
Old cached versions served until cache expires or is purged.
Recommended fix:
Control caching behavior:
// Force no caching (always fresh)
export const dynamic = 'force-dynamic'
export const revalidate = 0
// In Server Action:
async function updatePost(id: string) {
await db.post.update({ ... })
revalidatePath(/posts/${id}) // Purge this page
revalidateTag('posts') // Purge all with this tag
}