| 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 |
Vercel Deployment Standards
This skill provides complete guidelines for deploying applications to Vercel, with specific focus on Next.js optimizations and serverless architecture patterns.
Pre-Deployment Checklist
Repository Requirements
- Code pushed to GitHub/GitLab/Bitbucket
- Next.js project properly configured
-
package.jsonwith build scripts -
.gitignoreincludes.env,.vercel,node_modules - Dependencies correctly categorized
- Database migrations strategy defined
- API routes follow serverless best practices
Vercel-Specific Requirements
-
vercel.jsonconfigured (optional but recommended) - Environment variables documented
- Build output optimized
- Edge runtime considered for performance-critical routes
- ISR/SSG strategy defined
Initial Setup
Connect Repository
Via Dashboard:
- Go to vercel.com
- Click "Add New" → "Project"
- Import Git Repository
- Select repository
- Configure project settings
- Deploy
Via CLI:
# Install Vercel CLI
npm i -g vercel
# Login
vercel login
# Deploy
vercel
# Production deploy
vercel --prod
Project Configuration
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 * * *"
}
]
}
Environment Variables
Configuration
Via Dashboard:
- Project Settings → Environment Variables
- Add variables for each environment:
- Production
- Preview
- Development
Via CLI:
# Add environment variable
vercel env add DATABASE_URL production
# Pull environment variables locally
vercel env pull .env.local
Required Variables for Next.js
# 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/...
Environment Variable Types
Public (Client-Side):
- Must start with
NEXT_PUBLIC_ - Exposed to browser
- Embedded at build time
Private (Server-Side Only):
- No prefix needed
- Available in API routes and Server Components
- Never exposed to client
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 name
Build Configuration
Next.js Optimization
next.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
Build Performance
Optimize Build Times:
// package.json
{
"scripts": {
"build": "next build",
"postbuild": "next-sitemap"
}
}
Turbopack (Faster Builds):
# Use Turbopack for builds
next build --turbo
Serverless Functions
API Routes Configuration
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 |
Edge Functions
When to Use Edge:
- Authentication checks
- Geolocation-based routing
- A/B testing
- Bot detection
- Simple data fetching
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' }
})
}
Database Integration
Vercel Postgres
Setup:
- Dashboard → Storage → Create Database → Postgres
- Automatically provisions connection strings
- Environment variables auto-added
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 Integration
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
Supabase Integration
// 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!
)
Cron Jobs
Configuration
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:
- Every hour:
0 * * * * - Daily at 2 AM:
0 2 * * * - Every Monday:
0 0 * * 1 - First of month:
0 0 1 * *
Static & Dynamic Rendering
Rendering Strategies
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} />
}
Vercel KV (Redis)
Setup
- Dashboard → Storage → Create Database → KV
- Select region
- Environment variables auto-added
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 })
}
Vercel Blob (File Storage)
Setup & Usage
// 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)
Monitoring & Logs
Real-Time Logs
Via Dashboard:
- Project → Logs
- Filter by function, status, time range
- Search logs
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
Analytics
Web Analytics (Built-in):
- Dashboard → Analytics
- Page views, unique visitors
- Top pages, referrers
- Real-time data
Speed Insights:
- Core Web Vitals
- Performance metrics
- Real user monitoring
Enable in next.config.js:
module.exports = {
experimental: {
webVitalsAttribution: ['CLS', 'LCP', 'FCP', 'FID', 'TTFB']
}
}
Error Tracking
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,
})
Custom Domains
Setup
Add Domain:
- Project Settings → Domains
- Enter domain name
- Choose domain type (apex or subdomain)
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:
- Automatically provisioned
- Free via Let's Encrypt
- Auto-renewal
Performance Optimization
Image Optimization
// 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],
}
}
Edge Config (Feature Flags)
// 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')
Bundle Analysis
# 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
Deployment Strategies
Preview Deployments
Automatic:
- Every push to non-production branch creates preview
- Unique URL:
my-app-git-branch-username.vercel.app - Test changes before merging
Comments on PRs:
- Vercel bot comments with preview URL
- QA team can review
- Automatic updates on new commits
Production Deployments
Automatic:
- Push to
mainbranch triggers production deploy - Alias to custom domain
Manual:
# Deploy to production
vercel --prod
# Rollback to previous deployment
vercel rollback
Environment-Specific Builds
// 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',
}
Deployment Checklist
Pre-Deployment
- All environment variables set
- Database migrations tested
- Build passes locally (
npm run build) - No console errors or warnings
- Images optimized
- API routes tested
- Authentication working
- Third-party integrations configured
Post-Deployment
- Custom domain resolving correctly
- SSL certificate active
- Health check endpoint responding
- Database connection working
- Cron jobs running (check logs)
- Analytics tracking
- Error tracking configured
- Performance metrics monitored
- SEO meta tags correct
- Social share previews working
Troubleshooting
Build Errors
# 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
Runtime Errors
# Check:
- Function logs in dashboard
- Environment variables in deployment
- Database connection
- External API availability
Performance Issues
# Investigate:
- Bundle size (use bundle analyzer)
- Slow database queries
- Unoptimized images
- Blocking server-side operations
- Cold starts (consider edge runtime)
Best Practices
Security
- Use environment secrets
- Implement CSRF protection
- Rate limit API routes
- Validate all inputs
- Sanitize user content
- Use secure headers
- Keep dependencies updated
Performance
- Use ISR for dynamic content
- Implement edge caching
- Optimize images with next/image
- Minimize JavaScript bundle
- Use compression
- Lazy load components
- Prefetch critical routes
Cost Optimization
- Use Edge functions where possible (cheaper)
- Implement proper caching strategies
- Optimize serverless function duration
- Use ISR instead of SSR when possible
- Monitor bandwidth usage
- Clean up unused deployments
Resources
Pro Tip: Use preview deployments aggressively for QA. Every PR should have a working preview URL before merging to production.