Provides comprehensive Vercel deployment standards optimized for Next.js applications, covering environment configuration, edge functions, serverless architecture, database integration, cron jobs, and production best practices
This skill provides complete guidelines for deploying applications to Vercel, with specific focus on Next.js optimizations and serverless architecture patterns.
package.json with build scripts.gitignore includes .env, .vercel, node_modulesvercel.json configured (optional but recommended)Via Dashboard:
Via CLI:
# Install Vercel CLI
npm i -g vercel
# Login
vercel login
# Deploy
vercel
# Production deploy
vercel --prod
vercel.json:
{
"buildCommand": "npm run build",
"outputDirectory": ".next",
"framework": "nextjs",
"rewrites": [
{ "source": "/api/:path*", "destination": "/api/:path*" }
],
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "X-XSS-Protection",
"value": "1; mode=block"
}
]
}
],
"regions": ["iad1", "sfo1"],
"crons": [
{
"path": "/api/cron/daily-cleanup",
"schedule": "0 0 * * *"
}
]
}
Via Dashboard:
Via CLI:
# Add environment variable
vercel env add DATABASE_URL production
# Pull environment variables locally
vercel env pull .env.local
# Core Next.js
NODE_ENV=production # Auto-set by Vercel
NEXT_PUBLIC_VERCEL_URL=${VERCEL_URL} # Auto-provided
# Application URLs
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
NEXTAUTH_URL=https://yourdomain.com
NEXTAUTH_SECRET=your-secret-min-32-chars
# Database (Vercel Postgres / Supabase / PlanetScale)
DATABASE_URL=postgresql://...
POSTGRES_URL=postgresql://...
POSTGRES_PRISMA_URL=postgresql://... # For Prisma
POSTGRES_URL_NON_POOLING=postgresql://... # For migrations
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...
# External Services
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
SENDGRID_API_KEY=SG...
OPENAI_API_KEY=sk-...
# Storage (Vercel Blob)
BLOB_READ_WRITE_TOKEN=vercel_blob_...
# KV (Vercel KV - Redis)
KV_REST_API_URL=https://...
KV_REST_API_TOKEN=...
KV_REST_API_READ_ONLY_TOKEN=...
# Edge Config (Feature flags)
EDGE_CONFIG=https://edge-config.vercel.com/...
Public (Client-Side):
NEXT_PUBLIC_Private (Server-Side Only):
System Variables (Auto-provided by Vercel):
VERCEL_URL - Deployment URLVERCEL_ENV - production | preview | developmentVERCEL_GIT_COMMIT_SHA - Git commit hashVERCEL_GIT_COMMIT_REF - Git branch namenext.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
// Production optimizations
reactStrictMode: true,
swcMinify: true,
// Image optimization
images: {
domains: ['yourdomain.com', 'images.unsplash.com'],
formats: ['image/avif', 'image/webp'],
},
// Headers
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
}
]
}
]
},
// Redirects
async redirects() {
return [
{
source: '/old-page',
destination: '/new-page',
permanent: true,
}
]
},
// Experimental features
experimental: {
serverActions: {
bodySizeLimit: '2mb',
},
},
}
module.exports = nextConfig
Optimize Build Times:
// package.json
{
"scripts": {
"build": "next build",
"postbuild": "next-sitemap"
}
}
Turbopack (Faster Builds):
# Use Turbopack for builds
next build --turbo
Function Config:
// app/api/users/route.ts
import { NextResponse } from 'next/server'
export const runtime = 'edge' // or 'nodejs' (default)
export const maxDuration = 60 // seconds (Pro plan: 300s)
export const dynamic = 'force-dynamic' // Disable caching
export async function GET(request: Request) {
// Your logic here
return NextResponse.json({ users: [] })
}
Runtime Options:
| Runtime | Use Case | Execution Time | Cold Start |
|---|---|---|---|
edge | Low-latency, simple logic | 30s (Hobby), 900s (Pro) | ~5ms |
nodejs | Complex logic, libraries | 10s (Hobby), 300s (Pro) | ~100ms |
When to Use Edge:
Example:
// app/api/geo/route.ts
export const runtime = 'edge'
export async function GET(request: Request) {
const geo = request.headers.get('x-vercel-ip-country') || 'unknown'
return new Response(JSON.stringify({ country: geo }), {
headers: { 'content-type': 'application/json' }
})
}
Setup:
Environment Variables (Auto-provided):
POSTGRES_URL="postgresql://..."
POSTGRES_PRISMA_URL="postgresql://..." # With pgBouncer
POSTGRES_URL_NON_POOLING="postgresql://..." # Direct connection
Connection:
// lib/db.ts
import { Pool } from '@vercel/postgres'
export const db = new Pool({
connectionString: process.env.POSTGRES_URL,
})
// Usage
const { rows } = await db.query('SELECT * FROM users')
Prisma Setup:
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
Database Migrations:
// package.json
{
"scripts": {
"postinstall": "prisma generate",
"db:migrate": "prisma migrate deploy",
"vercel-build": "prisma generate && prisma migrate deploy && next build"
}
}
Vercel Build Settings:
# Build Command
npm run vercel-build
# This ensures migrations run before build
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
vercel.json:
{
"crons": [
{
"path": "/api/cron/daily-cleanup",
"schedule": "0 0 * * *"
},
{
"path": "/api/cron/send-reminders",
"schedule": "0 9 * * *"
},
{
"path": "/api/cron/weekly-report",
"schedule": "0 10 * * 1"
}
]
}
Cron API Route:
// app/api/cron/daily-cleanup/route.ts
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
// Verify cron secret
const authHeader = request.headers.get('authorization')
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
// Run cleanup logic
await cleanupOldData()
return NextResponse.json({ success: true })
} catch (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}
}
async function cleanupOldData() {
// Your cleanup logic
console.log('Running daily cleanup...')
}
Cron Schedule Format:
* * * * *
│ │ │ │ │
│ │ │ │ └─ Day of week (0-7)
│ │ │ └─── Month (1-12)
│ │ └───── Day of month (1-31)
│ └─────── Hour (0-23)
└───────── Minute (0-59)
Examples:
0 * * * *0 2 * * *0 0 * * 10 0 1 * *Static Generation (SSG):
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({ slug: post.slug }))
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
return <Article post={post} />
}
Incremental Static Regeneration (ISR):
// app/products/[id]/page.tsx
export const revalidate = 3600 // Revalidate every hour
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id)
return <Product data={product} />
}
Server-Side Rendering (SSR):
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic' // Always SSR
export default async function Dashboard() {
const session = await getSession()
const data = await getUserData(session.userId)
return <DashboardView data={data} />
}
Usage:
// lib/kv.ts
import { kv } from '@vercel/kv'
// Set value
await kv.set('user:123', { name: 'John' })
// Get value
const user = await kv.get('user:123')
// Set with expiration
await kv.setex('session:abc', 3600, { userId: '123' })
// Delete
await kv.del('user:123')
// Increment
await kv.incr('page:views')
Rate Limiting Example:
// app/api/protected/route.ts
import { kv } from '@vercel/kv'
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const ip = request.headers.get('x-forwarded-for') || 'unknown'
const key = `ratelimit:${ip}`
const requests = await kv.incr(key)
if (requests === 1) {
await kv.expire(key, 60) // 60 second window
}
if (requests > 10) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
)
}
// Process request
return NextResponse.json({ success: true })
}
// Upload file
import { put } from '@vercel/blob'
export async function POST(request: Request) {
const form = await request.formData()
const file = form.get('file') as File
const blob = await put(file.name, file, {
access: 'public',
token: process.env.BLOB_READ_WRITE_TOKEN,
})
return Response.json({ url: blob.url })
}
// List files
import { list } from '@vercel/blob'
const { blobs } = await list()
// Delete file
import { del } from '@vercel/blob'
await del(url)
Via Dashboard:
Via CLI:
# Stream logs
vercel logs my-app --follow
# Logs from specific deployment
vercel logs my-app --deployment dpl_xxx
# Filter by function
vercel logs my-app --function /api/users
Web Analytics (Built-in):
Speed Insights:
Enable in next.config.js:
module.exports = {
experimental: {
webVitalsAttribution: ['CLS', 'LCP', 'FCP', 'FID', 'TTFB']
}
}
Integrate Sentry:
// sentry.config.ts
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.VERCEL_ENV,
tracesSampleRate: 1.0,
})
Add Domain:
Configure DNS:
For subdomain (www, app, etc.):
Type: CNAME
Name: www
Value: cname.vercel-dns.com
TTL: 3600
For apex domain (yourdomain.com):
Type: A
Name: @
Value: 76.76.21.21
Type: AAAA (IPv6)
Name: @
Value: 2606:4700:4700::1111
SSL Certificate:
// Use Next.js Image component
import Image from 'next/image'
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // For above-the-fold images
placeholder="blur" // Better UX
/>
next.config.js:
module.exports = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
}
}
// lib/feature-flags.ts
import { get } from '@vercel/edge-config'
export async function isFeatureEnabled(flag: string): Promise<boolean> {
try {
return await get(flag) || false
} catch {
return false
}
}
// Usage
const newUIEnabled = await isFeatureEnabled('new-ui')
# Analyze bundle size
npm install -D @next/bundle-analyzer
# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({
// Your config
})
# Run analysis
ANALYZE=true npm run build
Automatic:
my-app-git-branch-username.vercel.appComments on PRs:
Automatic:
main branch triggers production deployManual:
# Deploy to production
vercel --prod
# Rollback to previous deployment
vercel rollback
// lib/config.ts
const config = {
apiUrl: process.env.VERCEL_ENV === 'production'
? 'https://api.yourdomain.com'
: process.env.VERCEL_ENV === 'preview'
? 'https://api-staging.yourdomain.com'
: 'http://localhost:3001',
}
npm run build)# Common issues:
- Missing dependencies → Check package.json
- TypeScript errors → Run tsc --noEmit
- Environment variable missing → Set in dashboard
- Out of memory → Upgrade plan or optimize build
# Check:
- Function logs in dashboard
- Environment variables in deployment
- Database connection
- External API availability
# Investigate:
- Bundle size (use bundle analyzer)
- Slow database queries
- Unoptimized images
- Blocking server-side operations
- Cold starts (consider edge runtime)
Pro Tip: Use preview deployments aggressively for QA. Every PR should have a working preview URL before merging to production.38:["$","$L40",null,{"content":"$41","frontMatter":{"name":"deploy-vercel","description":"Provides comprehensive Vercel deployment standards optimized for Next.js applications, covering environment configuration, edge functions, serverless architecture, database integration, cron jobs, and production best practices"}}]