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] --> AEnvironment 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.comRequired 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
- Automatic Deployment: Push to main branch triggers production deployment
- Preview Deployments: Pull requests create preview deployments
- Environment Variables: Set in Vercel dashboard
- Database Migrations: Run automatically via
postinstallscript
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 devProduction Migrations
# Generate Prisma client
npx prisma generate
# Deploy migrations to production
npx prisma migrate deploy
# Verify migration status
npx prisma migrate statusMigration 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
- Database Recovery: Restore from latest Neon backup
- File Recovery: Restore from Vercel deployment history
- Configuration Recovery: Restore environment variables from secure storage
Security Considerations
Environment Security
- Store sensitive variables in Vercel dashboard
- Never commit
.envfiles 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
- Build Failures: Check TypeScript errors and dependency issues
- Database Connections: Verify DATABASE_URL and connection pool limits
- API Timeouts: Increase function timeout limits in vercel.json
- 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 lsPerformance 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.