Common Deployment Issues
Quick reference for the most frequent deployment problems:GitHub Actions Failures
Build Failures Issue: Build fails with missing environment variables# Error in GitHub Actions logs:
Error: Environment variable DATABASE_URL is not defined- Check GitHub repository secrets
- Verify secret names match workflow exactly
- Add missing secrets in Repository Settings → Secrets and variables → Actions
# Required secrets in GitHub:
DATABASE_URL # ✅ Must exist
BETTER_AUTH_SECRET # ✅ Must exist
RESEND_API_KEY # ✅ Must exist
SUPABASE_ANON_KEY # ✅ Must exist
STRIPE_SECRET_KEY # ✅ Must exist
STRIPE_WEBHOOK_SECRET # ✅ Must exist
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY # ✅ Must exist# Error:
Type 'string | undefined' is not assignable to type 'string'// lib/env.ts
export const requiredEnvVars = {
DATABASE_URL: process.env.DATABASE_URL!,
BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET!,
// ... other required vars
}
// Check at app startup
Object.entries(requiredEnvVars).forEach(([key, value]) => {
if (!value) {
throw new Error(`Missing required environment variable: ${key}`)
}
})# Error:
✖ 12 problems (8 errors, 4 warnings)# Fix linting issues locally
pnpm lint:fix
# Or disable specific rules if needed
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
# Commit fixes
git add .
git commit -m "Fix linting issues"
git pushTesting Failures
Issue: Tests pass locally but fail in CI# Error:
FAIL src/services/__tests__/user-service.test.ts
Database connection failed// Ensure test environment uses correct DATABASE_URL
// In GitHub Actions workflow, tests use same DB as build
# Check that test database is accessible from GitHub Actions# Error:
Test timeout after 5000ms// In test files
test('should complete operation', async () => {
// ... test code
}, 10000) // Increase timeout to 10s
// Or configure globally in vitest.config.ts
export default defineConfig({
test: {
testTimeout: 10000
}
})Database Migration Failures
Issue: Migration fails in production# Error in GitHub Actions:
❌ Migration failed
relation "uuid-ossp" does not exist// In migration script, ensure UUID extension is created first
await db.execute(sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`)# Error:
Migration timeout after 30 seconds-- Instead of one large migration:
-- ALTER TABLE users ADD COLUMN a text, ADD COLUMN b text, ...;
-- Break into multiple migrations:
-- Migration 1:
ALTER TABLE users ADD COLUMN a text;
-- Migration 2:
ALTER TABLE users ADD COLUMN b text;Vercel Deployment Issues
Build and Function Issues
Issue: Vercel build timeout# Error in Vercel logs:
Build timeout after 15 minutes// next.config.ts - optimize build
const nextConfig = {
// Reduce build time
typescript: {
ignoreBuildErrors: false, // Keep false for safety
},
eslint: {
ignoreDuringBuilds: false, // Keep false for safety
},
// Optimize output
output: 'standalone', // For Docker deployment if needed
// Reduce bundle size
experimental: {
optimizePackageImports: ['lodash', 'date-fns'],
},
}# Error:
Function Timeout after 30 seconds// vercel.json
{
"functions": {
"app/api/**/*.ts": {
"maxDuration": 60 // Increase to 60 seconds
}
}
}// Optimize database queries
// Use connection pooling
import { Pool } from 'pg'
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // Maximum connections
idleTimeoutMillis: 30000,
})Environment Variable Issues
Issue: Environment variables not available in Vercel# Error:
process.env.STRIPE_SECRET_KEY is undefined- Check Vercel Project Settings → Environment Variables
- Ensure variables are set for correct environment (Production/Preview/Development)
- Verify NEXT_PUBLIC_ prefix for client-side variables
# Works in development, fails in production// Debug environment variables
console.log('Environment:', process.env.NODE_ENV)
console.log('Vercel Environment:', process.env.VERCEL_ENV)
console.log('Database URL exists:', !!process.env.DATABASE_URL)
// Environment-specific logic
if (process.env.VERCEL_ENV === 'production') {
// Production-specific configuration
} else if (process.env.VERCEL_ENV === 'preview') {
// Preview-specific configuration
}Database Connection Issues
Connection Problems
Issue: Database connection refused# Error:
connect ECONNREFUSED# Verify DATABASE_URL format
postgres://user:password@host:port/database?sslmode=require
# Test connection manually
psql $DATABASE_URL -c "SELECT 1"
# Common fixes:
# 1. Add ?sslmode=require for managed databases
# 2. Check firewall/security group settings
# 3. Verify credentials are correct# Error:
remaining connection slots are reserved for non-replication superuser connections// lib/db.ts
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
// Use connection pooling
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // Maximum number of connections
min: 5, // Minimum number of connections
idle: 10000, // Close connections after 10 seconds of inactivity
connectionTimeoutMillis: 5000,
})
export const db = drizzle(pool)Schema Issues
Issue: Schema out of sync# Error:
column "new_field" of relation "users" does not exist# Check migration status
pnpm db:migrate
# If still failing, check __drizzle_migrations table
psql $DATABASE_URL -c "SELECT * FROM __drizzle_migrations ORDER BY created_at DESC LIMIT 5"
# Re-run migrations if needed
pnpm db:push --force # Only in development!Authentication & Better Auth Issues
Authentication Failures
Issue: Better Auth throws session errors# Error:
Invalid session token// Check BETTER_AUTH_SECRET is set consistently
// Verify BETTER_AUTH_URL matches deployment URL
// In production:
BETTER_AUTH_URL=https://yourdomain.com
// In preview:
BETTER_AUTH_URL=https://yourapp-git-dev-team.vercel.app# Error:
OAuth provider error: invalid_client# Google OAuth Console:
# Add authorized redirect URIs:
# https://yourdomain.com/api/auth/callback/google
# https://yourapp-git-dev-team.vercel.app/api/auth/callback/google
# GitHub OAuth App:
# Update Authorization callback URL:
# https://yourdomain.com/api/auth/callback/githubSession Issues
Issue: Users logged out randomly# Error:
Session expired unexpectedly// Better Auth session config
export const auth = betterAuth({
session: {
expiresIn: 60 * 60 * 24 * 30, // 30 days
updateAge: 60 * 60 * 24, // Update every 24 hours
cookieCache: {
enabled: true,
maxAge: 60 * 5, // 5 minutes
},
},
// ...
})Stripe Payment Issues
Webhook Problems
Issue: Stripe webhooks failing# Error in Stripe Dashboard:
Webhook endpoint returned 500// app/api/auth/stripe/webhook/route.ts
export async function POST(request: Request) {
try {
const body = await request.text()
const signature = request.headers.get('stripe-signature')
console.log('Webhook received:', {
hasBody: !!body,
hasSignature: !!signature,
webhookSecret: !!process.env.STRIPE_WEBHOOK_SECRET
})
// Verify webhook signature
const event = stripe.webhooks.constructEvent(
body,
signature!,
process.env.STRIPE_WEBHOOK_SECRET!
)
console.log('Webhook event:', event.type)
// Handle event...
} catch (error) {
console.error('Webhook error:', error)
return new Response('Webhook Error', { status: 400 })
}
return new Response('OK', { status: 200 })
}# Error:
No signatures found matching the expected signature for payload// Ensure raw body is used for signature verification
// Don't parse JSON before signature verification
// Correct approach:
const body = await request.text() // Raw text body
const event = stripe.webhooks.constructEvent(body, signature, secret)
// Incorrect approach:
const body = await request.json() // Parsed JSON breaks signaturePayment Flow Issues
Issue: Payments not processing# Error:
Payment failed with status: requires_action// Handle different payment statuses
switch (paymentIntent.status) {
case 'requires_payment_method':
// Payment method required
break
case 'requires_action':
// 3D Secure authentication required
// Redirect user to authentication
break
case 'succeeded':
// Payment successful
break
case 'requires_confirmation':
// Confirm payment intent
break
}File Upload & Supabase Issues
Storage Problems
Issue: File uploads failing# Error:
Supabase storage: Unauthorized-- In Supabase SQL Editor:
-- Allow authenticated users to upload files
INSERT INTO storage.buckets (id, name, public)
VALUES ('your-bucket-name', 'Your Bucket', true);
-- Set bucket policy
CREATE POLICY "Allow authenticated uploads"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (bucket_id = 'your-bucket-name');
-- Allow public read access
CREATE POLICY "Allow public reads"
ON storage.objects FOR SELECT
TO public
USING (bucket_id = 'your-bucket-name');# Error:
File size exceeds maximum allowed size// Environment variable
MAX_FILE_SIZE=5242880 // 5MB in bytes
// In upload handler
const maxSize = parseInt(process.env.MAX_FILE_SIZE || '5242880')
if (file.size > maxSize) {
throw new Error(`File size exceeds ${maxSize} bytes`)
}Performance & Optimization Issues
Slow Performance
Issue: Application loading slowly# Symptoms:
# - Long Time to First Byte (TTFB)
# - Slow page loads
# - High bounce rate// next.config.ts optimizations
const nextConfig = {
// Image optimization
images: {
formats: ['image/webp', 'image/avif'],
minimumCacheTTL: 60,
},
// Compression
compress: true,
// Caching headers
async headers() {
return [
{
source: '/api/:path*',
headers: [
{
key: 'Cache-Control',
value: 's-maxage=60, stale-while-revalidate=300'
}
]
}
]
},
// Static optimization
experimental: {
optimizePackageImports: ['lodash', '@radix-ui/react-icons'],
},
}Database Performance
Issue: Slow database queries# Symptoms:
# - API endpoints timing out
# - Long response times
# - High database CPU// Add database indexes
await db.execute(sql`
CREATE INDEX IF NOT EXISTS idx_user_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_subscription_user_id ON subscription(reference_id);
CREATE INDEX IF NOT EXISTS idx_organization_slug ON organization(slug);
`)
// Use pagination for large datasets
const users = await db.select()
.from(usersTable)
.limit(20)
.offset(page * 20)
// Use select only needed columns
const users = await db.select({
id: usersTable.id,
name: usersTable.name,
email: usersTable.email,
})
.from(usersTable)Debugging Techniques
Logging and Monitoring
Application Logging:// lib/logger.ts
import winston from 'winston'
export const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
// Add file transport for production
],
})
// Usage in API routes
import { logger } from '@/lib/logger'
export async function POST(request: Request) {
logger.info('API endpoint called', {
url: request.url,
method: request.method,
headers: Object.fromEntries(request.headers.entries())
})
try {
// ... API logic
logger.info('API endpoint completed successfully')
} catch (error) {
logger.error('API endpoint failed', {
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined
})
throw error
}
}// Enable query logging in development
const db = drizzle(client, {
logger: process.env.NODE_ENV === 'development' ? {
logQuery: (query, params) => {
console.log('Query:', query)
console.log('Params:', params)
}
} : undefined
})Health Check Endpoints
Create comprehensive health checks:// app/api/health/route.ts
export async function GET() {
const checks = {
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV,
version: process.env.VERCEL_GIT_COMMIT_SHA?.slice(0, 8) || 'unknown',
status: 'healthy',
checks: {} as Record<string, any>,
}
try {
// Database check
const dbStart = Date.now()
await db.select().from(users).limit(1)
checks.checks.database = {
status: 'healthy',
responseTime: Date.now() - dbStart
}
// Email service check
if (process.env.RESEND_API_KEY) {
checks.checks.email = { status: 'configured' }
}
// Stripe check
if (process.env.STRIPE_SECRET_KEY) {
checks.checks.stripe = { status: 'configured' }
}
// File storage check
if (process.env.SUPABASE_URL) {
checks.checks.storage = { status: 'configured' }
}
return Response.json(checks)
} catch (error) {
checks.status = 'unhealthy'
checks.checks.database = {
status: 'unhealthy',
error: error instanceof Error ? error.message : 'Unknown error'
}
return Response.json(checks, { status: 503 })
}
}Emergency Procedures
When production is completely broken:-
Immediate Rollback:
# Via Vercel Dashboard: # 1. Go to Project → Deployments # 2. Find last working deployment # 3. Click "Promote to Production" # Via CLI: vercel ls vercel promote [previous-deployment-url] -
Check System Status:
# Health check curl https://yourdomain.com/api/health # Database connectivity psql $DATABASE_URL -c "SELECT 1" # External services curl -H "Authorization: Bearer $RESEND_API_KEY" https://api.resend.com/domains -
Monitor and Fix:
# Check GitHub Actions logs # Review Vercel function logs # Monitor database performance # Check external service status pages