Troubleshooting Deployment

Comprehensive troubleshooting guide for common deployment issues and their solutions.

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
Solution:
  1. Check GitHub repository secrets
  2. Verify secret names match workflow exactly
  3. 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
Issue: Build succeeds locally but fails in CI
# Error:
Type 'string | undefined' is not assignable to type 'string'
Solution: Add environment variable validation
// 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}`)
  }
})
Linting Failures Issue: ESLint errors block deployment
# Error:
 12 problems (8 errors, 4 warnings)
Solution:
# 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 push

Testing Failures

Issue: Tests pass locally but fail in CI
# Error:
FAIL src/services/__tests__/user-service.test.ts
Database connection failed
Solution: Check test database configuration
// 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
Issue: Test timeout in CI environment
# Error:
Test timeout after 5000ms
Solution: Increase timeout and optimize tests
// 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
Solution: Ensure UUID extension creation
// In migration script, ensure UUID extension is created first
await db.execute(sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`)
Issue: Migration timeout
# Error:
Migration timeout after 30 seconds
Solution: Break large migrations into smaller chunks
-- 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
Solution: Optimize build process
// 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'],
  },
}
Issue: Serverless function timeout
# Error:
Function Timeout after 30 seconds
Solution: Optimize function performance and increase timeout
// 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
Solution:
  1. Check Vercel Project Settings → Environment Variables
  2. Ensure variables are set for correct environment (Production/Preview/Development)
  3. Verify NEXT_PUBLIC_ prefix for client-side variables
Issue: Different behavior between Vercel environments
# Works in development, fails in production
Solution: Check environment-specific configuration
// 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
Solution: Check database URL and network access
# 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
Issue: Too many database connections
# Error:
remaining connection slots are reserved for non-replication superuser connections
Solution: Implement connection pooling
// 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
Solution: Ensure migrations are applied
# 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
Solution: Check Better Auth configuration
// 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
Issue: OAuth providers not working
# Error:
OAuth provider error: invalid_client
Solution: Update OAuth provider settings
# 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/github

Session Issues

Issue: Users logged out randomly
# Error:
Session expired unexpectedly
Solution: Check session configuration
// 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
Solution: Debug webhook endpoint
// 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 })
}
Issue: Webhook signature verification fails
# Error:
No signatures found matching the expected signature for payload
Solution: Check webhook secret and payload handling
// 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 signature

Payment Flow Issues

Issue: Payments not processing
# Error:
Payment failed with status: requires_action
Solution: Handle 3D Secure authentication
// 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
Solution: Check Supabase bucket policies
-- 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');
Issue: File size limit exceeded
# Error:
File size exceeds maximum allowed size
Solution: Configure file size limits
// 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
Solution: Implement performance optimizations
// 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
Solution: Optimize database queries
// 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
  }
}
Database Query Debugging:
// 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:
  1. 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]
  2. 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
  3. Monitor and Fix:
    # Check GitHub Actions logs
    # Review Vercel function logs
    # Monitor database performance
    # Check external service status pages
Your deployment troubleshooting toolkit is now complete. These solutions cover 95% of common deployment issues you'll encounter. Remember to always test fixes in preview environments before applying to production.
    Troubleshooting Deployment | ShipSaaS Documentation | ShipSaaS - Launch your SaaS with AI in days