Docs
Deployment & Scripts

Deployment & Scripts

Comprehensive guide to deployment strategies, build processes, environment management, and utility scripts for the YouTube Analyzer application.

This section covers all aspects of deploying and maintaining the YouTube Analyzer application, including build processes, environment management, database scripts, and operational procedures.

Deployment Architecture

Production Stack

graph TD
    A[Vercel] --> B[Next.js App]
    B --> C[Neon Database]
    B --> D[Stripe API]
    B --> E[Resend Email]
    B --> F[YouTube API]
    B --> G[OpenAI API]
    
    H[Domain] --> A
    I[CDN] --> A
    J[Edge Functions] --> A

Environment Strategy

  • Development: Local development with SQLite or local PostgreSQL
  • Staging: Branch previews on Vercel with staging database
  • Production: Main branch deployed to Vercel with production database

Environment Configuration

Environment Files

# .env.local (development)
DATABASE_URL=postgresql://user:password@localhost:5432/youtube_analyzer_dev
NEXTAUTH_SECRET=your_development_secret
NEXTAUTH_URL=http://localhost:3000
 
# .env.production (via Vercel)
DATABASE_URL=postgresql://user:password@ep-xyz.neon.tech/neon_db
NEXTAUTH_SECRET=your_production_secret
NEXTAUTH_URL=https://yourdomain.com

Required Environment Variables

// env.mjs - Environment validation
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
 
export const env = createEnv({
  server: {
    // Database
    DATABASE_URL: z.string().url(),
    
    // Authentication
    NEXTAUTH_SECRET: z.string().min(1),
    NEXTAUTH_URL: z.preprocess(
      (str) => process.env.VERCEL_URL ?? str,
      z.string().url()
    ),
    
    // External APIs
    YOUTUBE_API_KEY: z.string().min(1),
    OPENAI_API_KEY: z.string().min(1),
    STRIPE_API_KEY: z.string().min(1),
    STRIPE_WEBHOOK_SECRET: z.string().min(1),
    RESEND_API_KEY: z.string().min(1),
    
    // OAuth
    GOOGLE_CLIENT_ID: z.string().min(1),
    GOOGLE_CLIENT_SECRET: z.string().min(1),
    
    // Email
    EMAIL_FROM: z.string().email(),
  },
  client: {
    NEXT_PUBLIC_APP_URL: z.string().url(),
  },
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
    NEXTAUTH_URL: process.env.NEXTAUTH_URL,
    // ... all environment variables
  },
});

Build Configuration

Next.js Configuration

// next.config.js
/** @type {import('next').NextConfig} */
const { withContentlayer } = require("next-contentlayer");
 
const nextConfig = {
  experimental: {
    serverComponentsExternalPackages: ["@prisma/client"],
  },
  images: {
    domains: [
      "avatars.githubusercontent.com",
      "lh3.googleusercontent.com",
      "i.ytimg.com", // YouTube thumbnails
    ],
  },
  webpack: (config) => {
    config.resolve.fallback = {
      ...config.resolve.fallback,
      fs: false,
    };
    return config;
  },
};
 
module.exports = withContentlayer(nextConfig);

TypeScript Configuration

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["dom", "dom.iterable", "ES6"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [{ "name": "next" }],
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
 
// tsconfig.scripts.json - For standalone scripts
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "commonjs",
    "esModuleInterop": true,
    "allowImportingTsExtensions": true,
    "noEmit": false,
    "outDir": "./dist-scripts"
  },
  "include": ["scripts/**/*"],
  "exclude": ["node_modules"]
}

Package.json Scripts

{
  "scripts": {
    // Development
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "type-check": "tsc --noEmit",
    
    // Database
    "db:generate": "prisma generate",
    "db:push": "prisma db push",
    "db:migrate": "prisma migrate dev",
    "db:studio": "prisma studio",
    "db:reset": "prisma migrate reset",
    
    // Scripts
    "script:backup": "tsx scripts/backup-dev-data.ts",
    "script:restore": "tsx scripts/restore-dev-data.ts",
    "script:seed": "tsx scripts/seed-from-prod.ts",
    "script:export": "tsx scripts/export-prod-data.ts",
    
    // Email
    "email": "email dev",
    "email:build": "email export",
    
    // Production
    "build:production": "npm run db:generate && npm run build",
    "postinstall": "prisma generate"
  }
}

Database Scripts

Backup Script

// scripts/backup-dev-data.ts
import { PrismaClient } from '@prisma/client';
import fs from 'fs';
import path from 'path';
 
const prisma = new PrismaClient();
 
async function backupData() {
  try {
    console.log('🔄 Starting database backup...');
    
    // Get all data from each table
    const users = await prisma.user.findMany();
    const accounts = await prisma.account.findMany();
    const sessions = await prisma.session.findMany();
    const verificationTokens = await prisma.verificationToken.findMany();
    const analyses = await prisma.analysis.findMany();
    const autoAnalyses = await prisma.autoAnalysis.findMany();
    const videos = await prisma.video.findMany();
 
    const backup = {
      timestamp: new Date().toISOString(),
      data: {
        users,
        accounts, 
        sessions,
        verificationTokens,
        analyses,
        autoAnalyses,
        videos,
      }
    };
 
    // Write backup to file
    const backupPath = path.join(process.cwd(), 'dev-data-backup.json');
    fs.writeFileSync(backupPath, JSON.stringify(backup, null, 2));
 
    console.log('✅ Backup completed successfully!');
    console.log(`📁 Backup saved to: ${backupPath}`);
    console.log('📊 Data counts:');
    console.log(`   Users: ${users.length}`);
    console.log(`   Analyses: ${analyses.length}`);
    console.log(`   Videos: ${videos.length}`);
  } catch (error) {
    console.error('❌ Backup failed:', error);
    process.exit(1);
  } finally {
    await prisma.$disconnect();
  }
}
 
backupData();

Restore Script

// scripts/restore-dev-data.ts
import { PrismaClient } from '@prisma/client';
import fs from 'fs';
import path from 'path';
 
const prisma = new PrismaClient();
 
async function restoreData() {
  try {
    // Validate environment
    if (process.env.NODE_ENV === 'production') {
      throw new Error('❌ Cannot run restore script in production environment!');
    }
 
    console.log('🔄 Starting database restore...');
    
    const backupPath = path.join(process.cwd(), 'dev-data-backup.json');
    
    if (!fs.existsSync(backupPath)) {
      throw new Error(`❌ Backup file not found: ${backupPath}`);
    }
 
    const backupData = JSON.parse(fs.readFileSync(backupPath, 'utf8'));
    
    console.log(`📅 Restoring backup from: ${backupData.timestamp}`);
 
    // Clear existing data (in reverse order of dependencies)
    await prisma.video.deleteMany();
    await prisma.autoAnalysis.deleteMany();
    await prisma.analysis.deleteMany();
    await prisma.verificationToken.deleteMany();
    await prisma.session.deleteMany();
    await prisma.account.deleteMany();
    await prisma.user.deleteMany();
 
    // Restore data using upsert for safety
    for (const user of backupData.data.users) {
      await prisma.user.upsert({
        where: { id: user.id },
        update: user,
        create: user,
      });
    }
 
    for (const account of backupData.data.accounts) {
      await prisma.account.upsert({
        where: { 
          provider_providerAccountId: {
            provider: account.provider,
            providerAccountId: account.providerAccountId,
          }
        },
        update: account,
        create: account,
      });
    }
 
    // Continue for other tables...
 
    console.log('✅ Restore completed successfully!');
  } catch (error) {
    console.error('❌ Restore failed:', error);
    process.exit(1);
  } finally {
    await prisma.$disconnect();
  }
}
 
restoreData();

Production Data Export

// scripts/export-prod-data.ts
import { PrismaClient } from '@prisma/client';
import fs from 'fs';
import path from 'path';
 
const prisma = new PrismaClient();
 
async function exportProdData() {
  try {
    // Validate we're connecting to production
    if (!process.env.DATABASE_URL?.includes('neon.tech')) {
      console.log('⚠️  Warning: DATABASE_URL does not appear to be production');
      // Require explicit confirmation
      const readline = require('readline').createInterface({
        input: process.stdin,
        output: process.stdout
      });
      
      const answer = await new Promise((resolve) => {
        readline.question('Continue? (yes/no): ', resolve);
      });
      
      if (answer !== 'yes') {
        console.log('❌ Export cancelled');
        process.exit(0);
      }
    }
 
    console.log('🔄 Exporting production data...');
    
    // Export aggregated/anonymized data only
    const userCount = await prisma.user.count();
    const analysisCount = await prisma.analysis.count();
    const videoCount = await prisma.video.count();
    
    // Export recent analyses (last 30 days, anonymized)
    const recentAnalyses = await prisma.analysis.findMany({
      where: {
        createdAt: {
          gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
        },
      },
      select: {
        id: true,
        type: true,
        status: true,
        createdAt: true,
        completedAt: true,
        videoCount: true,
        channelTitle: true,
        // Exclude user data and content
      },
    });
 
    const exportData = {
      timestamp: new Date().toISOString(),
      stats: {
        totalUsers: userCount,
        totalAnalyses: analysisCount,
        totalVideos: videoCount,
      },
      recentAnalyses: recentAnalyses.map(analysis => ({
        ...analysis,
        id: `analysis_${Math.random().toString(36).substr(2, 9)}`, // Anonymize ID
      })),
    };
 
    const exportPath = path.join(process.cwd(), 'prod-data.json');
    fs.writeFileSync(exportPath, JSON.stringify(exportData, null, 2));
 
    console.log('✅ Production data exported successfully!');
    console.log(`📁 Export saved to: ${exportPath}`);
  } catch (error) {
    console.error('❌ Export failed:', error);
    process.exit(1);
  } finally {
    await prisma.$disconnect();
  }
}
 
exportProdData();

Utility Scripts

// scripts/cancel-stuck-analyses.ts
import { PrismaClient } from '@prisma/client';
 
const prisma = new PrismaClient();
 
async function cancelStuckAnalyses() {
  try {
    console.log('🔄 Finding stuck analyses...');
    
    // Find analyses that have been "processing" for more than 1 hour
    const stuckAnalyses = await prisma.analysis.findMany({
      where: {
        status: 'processing',
        createdAt: {
          lt: new Date(Date.now() - 60 * 60 * 1000), // 1 hour ago
        },
      },
    });
 
    console.log(`Found ${stuckAnalyses.length} stuck analyses`);
 
    if (stuckAnalyses.length > 0) {
      const result = await prisma.analysis.updateMany({
        where: {
          id: {
            in: stuckAnalyses.map(a => a.id),
          },
        },
        data: {
          status: 'failed',
          completedAt: new Date(),
        },
      });
 
      console.log(`✅ Updated ${result.count} stuck analyses to failed status`);
    }
  } catch (error) {
    console.error('❌ Failed to cancel stuck analyses:', error);
    process.exit(1);
  } finally {
    await prisma.$disconnect();
  }
}
 
cancelStuckAnalyses();

Vercel Deployment

Vercel Configuration

// vercel.json
{
  "framework": "nextjs",
  "buildCommand": "npm run build:production",
  "devCommand": "npm run dev",
  "installCommand": "npm install",
  "functions": {
    "app/api/analysis/route.ts": {
      "maxDuration": 300
    },
    "app/api/webhooks/stripe/route.ts": {
      "maxDuration": 30
    }
  },
  "crons": [
    {
      "path": "/api/cron/cleanup",
      "schedule": "0 2 * * *"
    },
    {
      "path": "/api/cron/auto-analysis",
      "schedule": "0 */6 * * *"
    }
  ]
}

Deployment Process

  1. Automatic Deployment: Push to main branch triggers production deployment
  2. Preview Deployments: Pull requests create preview deployments
  3. Environment Variables: Set in Vercel dashboard
  4. Database Migrations: Run automatically via postinstall script

Pre-deployment Checklist

# 1. Run type checking
npm run type-check
 
# 2. Run linting
npm run lint
 
# 3. Test build locally
npm run build
 
# 4. Check database schema
npm run db:generate
 
# 5. Run tests
npm test
 
# 6. Verify environment variables
node -e "console.log(Object.keys(process.env).filter(k => k.includes('NEXT_PUBLIC')))"

Database Migration Strategy

Development Migrations

# Create new migration
npx prisma migrate dev --name add_new_field
 
# Reset database (development only)
npx prisma migrate reset
 
# Apply pending migrations
npx prisma migrate dev

Production Migrations

# Generate Prisma client
npx prisma generate
 
# Deploy migrations to production
npx prisma migrate deploy
 
# Verify migration status
npx prisma migrate status

Migration Safety

// Safe migration patterns
model User {
  id String @id @default(cuid())
  email String @unique
  name String?
  
  // ✅ Safe: Adding nullable field
  newField String?
  
  // ✅ Safe: Adding field with default
  createdAt DateTime @default(now())
  
  // ❌ Risky: Dropping field (requires data migration)
  // oldField String? // Remove in separate migration
}

Monitoring & Logging

Application Monitoring

// lib/monitoring.ts
export function logError(error: Error, context: Record<string, any>) {
  console.error('Application Error:', {
    message: error.message,
    stack: error.stack,
    context,
    timestamp: new Date().toISOString(),
    environment: process.env.NODE_ENV,
  });
  
  // Send to monitoring service in production
  if (process.env.NODE_ENV === 'production') {
    // Sentry, LogRocket, etc.
  }
}
 
export function logAnalysisMetrics(analysis: {
  id: string;
  duration: number;
  videoCount: number;
  tokensUsed: number;
}) {
  console.log('Analysis Metrics:', {
    analysisId: analysis.id,
    duration: `${analysis.duration}ms`,
    videoCount: analysis.videoCount,
    tokensUsed: analysis.tokensUsed,
    timestamp: new Date().toISOString(),
  });
}

Performance Monitoring

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
 
export function middleware(request: NextRequest) {
  const start = Date.now();
  
  const response = NextResponse.next();
  
  // Add performance headers
  const duration = Date.now() - start;
  response.headers.set('X-Response-Time', `${duration}ms`);
  
  // Log slow requests
  if (duration > 1000) {
    console.warn('Slow request:', {
      url: request.url,
      duration: `${duration}ms`,
      userAgent: request.headers.get('user-agent'),
    });
  }
  
  return response;
}
 
export const config = {
  matcher: '/api/:path*',
};

Backup & Recovery

Automated Backups

// app/api/cron/backup/route.ts
export async function GET() {
  try {
    // Only run in production
    if (process.env.NODE_ENV !== 'production') {
      return Response.json({ error: 'Not in production' }, { status: 400 });
    }
    
    // Create database backup
    const backup = await createDatabaseBackup();
    
    // Store in cloud storage
    await storageService.upload(`backups/daily-${Date.now()}.sql`, backup);
    
    return Response.json({ success: true });
  } catch (error) {
    console.error('Backup failed:', error);
    return Response.json({ error: 'Backup failed' }, { status: 500 });
  }
}

Recovery Procedures

  1. Database Recovery: Restore from latest Neon backup
  2. File Recovery: Restore from Vercel deployment history
  3. Configuration Recovery: Restore environment variables from secure storage

Security Considerations

Environment Security

  • Store sensitive variables in Vercel dashboard
  • Never commit .env files to git
  • Use different API keys for each environment
  • Rotate secrets regularly

Deployment Security

// Security headers
export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  
  // Security headers
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  response.headers.set(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'"
  );
  
  return response;
}

Troubleshooting

Common Deployment Issues

  1. Build Failures: Check TypeScript errors and dependency issues
  2. Database Connections: Verify DATABASE_URL and connection pool limits
  3. API Timeouts: Increase function timeout limits in vercel.json
  4. Environment Variables: Ensure all required variables are set

Debug Commands

# Check Vercel deployment logs
vercel logs
 
# Test database connection
npx prisma db pull
 
# Verify build locally
npm run build
 
# Check environment variables
vercel env ls

Performance Optimization

  • Enable Vercel Edge Functions for global distribution
  • Implement database connection pooling
  • Use Next.js Image Optimization
  • Configure proper caching headers
  • Monitor Core Web Vitals

This deployment guide ensures reliable, secure, and maintainable deployments of the YouTube Analyzer application across all environments.