Backend Structure
Comprehensive overview of the backend architecture, API routes, database models, and server-side functionality.
Backend Structure
This document provides a detailed overview of the backend architecture, including API routes, database models, service layers, and server-side functionality that powers the YouTube Analyzer application.
Overview
The backend is built with Next.js 14 API Routes, providing a serverless architecture that scales automatically. The system uses PostgreSQL with Prisma ORM for data persistence, and integrates with multiple external services for YouTube data, AI processing, payments, and email delivery.
API Routes Architecture
Route Organization (app/api/)
The API routes are organized by feature and functionality:
app/api/
├── analysis/ # Manual analysis endpoints
│ ├── route.ts # POST: Create new analysis
│ ├── [id]/ # Dynamic analysis management
│ │ └── route.ts # GET: Fetch analysis, DELETE: Cancel analysis
│ ├── cancel/ # Cancel stuck analyses
│ └── check/ # Polling for new records
├── auto-analysis/ # Automated analysis management
│ ├── route.ts # POST: Create, GET: List auto-analyses
│ └── [id]/ # Individual auto-analysis management
│ └── route.ts # PATCH: Update, DELETE: Remove
├── dashboard/ # Dashboard data endpoints
│ ├── recent/ # Recent analyses (limit 5)
│ └── stats/ # Dashboard statistics
├── user/ # User management
│ └── route.ts # DELETE: User deletion
├── webhooks/ # External service webhooks
│ └── stripe/ # Stripe payment webhooks
├── cron/ # Scheduled task endpoints
│ └── auto-analysis/ # Auto-analysis cron job
├── share-analysis/ # Analysis sharing via email
├── update-analysis/ # Analysis result updates
└── auth/ # Authentication endpoints
└── [...nextauth]/ # Auth.js authentication
Core API Endpoints
1. Analysis Management (/api/analysis/)
POST /api/analysis - Create New Analysis
interface AnalysisRequest {
channelUrl: string;
videoCount: number;
}
interface AnalysisResponse {
success: boolean;
analysisId?: string;
error?: string;
}Features:
- Credit validation and reservation
- Channel URL parsing and validation
- Integration with analysis processing pipeline
- Real-time credit tracking
Implementation:
export async function POST(req: NextRequest) {
const user = await getCurrentUser();
if (!user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { channelUrl, videoCount } = await req.json();
const cost = CREDIT_COSTS[videoCount] || videoCount;
// Validate user credits
const dbUser = await prisma.user.findUnique({ where: { id: user.id } });
if (!dbUser || dbUser.credits < cost) {
return NextResponse.json({ error: "Insufficient credits" }, { status: 400 });
}
// Process analysis and deduct credits
// ... processing logic
}GET /api/analysis/[id] - Fetch Analysis Results
interface AnalysisWithVideos {
id: string;
channelUrl: string;
htmlResult: string;
status: 'processing' | 'completed' | 'failed';
videos: VideoData[];
createdAt: string;
}2. Auto-Analysis Management (/api/auto-analysis/)
POST /api/auto-analysis - Create Auto-Analysis Configuration
interface AutoAnalysisRequest {
channelId: string;
channelName: string;
channelUrl: string;
triggerType: 'per_video' | 'batch_3' | 'batch_5';
}Features:
- Subscription plan validation (paid plans only)
- Duplicate prevention per user/channel
- Configurable trigger conditions
GET /api/auto-analysis - List User's Auto-Analyses
interface AutoAnalysisResponse {
autoAnalyses: Array<{
id: string;
channelName: string;
triggerType: string;
isActive: boolean;
analysisCount: number;
lastRunAt?: string;
}>;
}PATCH /api/auto-analysis/[id] - Update Configuration
- Toggle active/inactive status
- Change trigger type
- Update configuration parameters
3. Dashboard Data (/api/dashboard/)
GET /api/dashboard/recent - Recent Analyses
interface RecentAnalysesResponse {
recentAnalyses: Array<{
id: string;
channel_name: string;
channelUrl: string;
status: string;
createdAt: string;
video_thumbnail?: string;
}>;
}GET /api/dashboard/stats - Dashboard Statistics
interface DashboardStats {
monthlyAnalyses: number;
processingCount: number;
apiCreditsRemaining: number;
hasActiveSubscription: boolean;
}4. YouTube Data Processing (/api/youtube/process)
This is the core endpoint that handles the entire analysis workflow:
interface YouTubeProcessRequest {
channel: string;
count: number;
analysisType?: string;
}
interface YouTubeProcessResponse {
success: boolean;
analysisId: string;
status: 'processing' | 'completed';
}Process Flow:
- Credit Validation: Check and reserve user credits
- Channel Resolution: Parse and validate YouTube channel
- Data Collection: Fetch videos, transcripts, and metadata
- AI Processing: Generate analysis using OpenAI
- Data Storage: Save results and update database
- Credit Deduction: Deduct credits only after successful completion
5. Webhook Handlers (/api/webhooks/)
POST /api/webhooks/stripe - Stripe Payment Webhooks
interface StripeWebhookEvents {
'checkout.session.completed': PaymentSuccessHandler;
'invoice.payment_succeeded': SubscriptionRenewalHandler;
'customer.subscription.updated': SubscriptionChangeHandler;
}Features:
- Secure webhook signature validation
- Subscription status updates
- Credit replenishment for paid plans
- Customer data synchronization
Admin Tools & Prompt Management
The backend includes dedicated logic and API endpoints for admin-only prompt management:
- All prompt management endpoints are under
/app/api/prompts/and handled inlib/prompts.ts. - Admins can create, edit, and version prompt templates used by the AI analysis workflow.
- All changes are versioned, auditable, and restricted to users with the
ADMINrole.
See the Admin Tools documentation for a full technical breakdown of backend prompt management and other admin features.
Service Layer Architecture
Core Services (lib/)
1. Database Service (lib/db.ts)
// Prisma client with connection pooling
export const prisma: PrismaClient;
// Connection management for development vs production
if (process.env.NODE_ENV === "production") {
prisma = new PrismaClient();
} else {
// Development: use global cache to prevent connection exhaustion
if (!global.cachedPrisma) {
global.cachedPrisma = new PrismaClient();
}
prisma = global.cachedPrisma;
}2. YouTube Integration (lib/youtube.ts)
interface YouTubeVideo {
id: string;
publishedAt: string;
title: string;
description: string;
thumbnail: string;
duration: string;
viewCount: number;
likeCount: number;
}
// Core YouTube API functions
export async function getChannelVideos(
channelId: string,
maxResults: number
): Promise<YouTubeVideo[]>;
export async function getVideoTranscript(
videoId: string
): Promise<string>;
export async function analyzeCommentsSentiment(
videoId: string
): Promise<SentimentAnalysis>;Features:
- YouTube Data API v3 integration
- Automatic transcript extraction
- Comment sentiment analysis
- Rate limiting and error handling
- Mock data support for testing
3. OpenAI Integration (lib/openai-streaming.ts)
interface AnalysisPrompt {
channelName: string;
videoCount: number;
performanceData: string;
combinedTranscripts: string;
videoLinksWithData: string;
}
export async function generateAnalysis(
prompt: AnalysisPrompt,
analysisType: string
): Promise<string>;
export async function streamAnalysisToUser(
prompt: AnalysisPrompt,
onChunk: (chunk: string) => void
): Promise<void>;Features:
- GPT-4 powered content analysis
- Streaming responses for real-time feedback
- Multiple analysis types with different prompts
- Token optimization and cost management
4. Email Service (lib/email.ts)
interface EmailTemplate {
to: string;
subject: string;
react: ReactElement;
}
export async function sendAnalysisResultEmail(
analysis: Analysis,
userEmail: string
): Promise<void>;
export async function sendWelcomeEmail(
user: User
): Promise<void>;Integration with Resend and React Email:
- HTML email templates using React components
- Transactional email delivery
- Email tracking and analytics
- Template management system
5. Stripe Integration (lib/stripe.ts)
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function createCheckoutSession(
userId: string,
priceId: string
): Promise<Stripe.Checkout.Session>;
export async function createCustomerPortalSession(
customerId: string
): Promise<Stripe.BillingPortal.Session>;6. Subscription Management (lib/subscription.ts)
interface SubscriptionPlan {
name: string;
credits: number;
isPaid: boolean;
features: string[];
}
export async function getUserSubscriptionPlan(
userId: string
): Promise<SubscriptionPlan>;
export async function updateUserCredits(
userId: string,
credits: number
): Promise<void>;7. Subscription Features (lib/subscription-features.ts)
Centralized feature access system with admin bypasses:
// Core feature checking functions
export function canUseAutomation(
userPlan: UserPlan | null,
isAdmin: boolean = false
): boolean;
export function canUseEmailNotifications(
userPlan: UserPlan | null,
isAdmin: boolean = false
): boolean;
export function canUseAnalysisType(
analysisType: string,
userPlan: UserPlan | null,
isAdmin: boolean = false
): boolean;
export function hasEnoughCredits(
analysisType: string,
userPlan: UserPlan | null,
isAdmin: boolean = false
): boolean;Key Features:
- Admin bypass for all feature restrictions
- Type-safe integration with config files
- Consistent API across frontend and backend
- Single source of truth for subscription logic
Database Models and Relationships
Core Models (prisma/schema.prisma)
1. User Model
model User {
id String @id @default(cuid())
name String?
email String @unique
role UserRole @default(USER)
credits Int @default(3)
creditsUsed Int @default(0)
stripeCustomerId String? @unique
stripeSubscriptionId String? @unique
stripePriceId String?
stripeCurrentPeriodEnd DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relationships
accounts Account[]
sessions Session[]
analyses Analysis[]
autoAnalyses AutoAnalysis[]
}2. Analysis Model
model Analysis {
id String @id @default(cuid())
userId String
channelUrl String
channelId String?
channel_name String?
channel_thumbnail String?
videoCount Int
video_count Int?
htmlResult String? @db.Text
status String @default("processing")
error_message String?
summary_preview String?
triggerType String? // "manual", "per_video", "batch_3", "batch_5"
autoAnalysisId String?
analysisType String @default("standard-summary")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relationships
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
autoAnalysis AutoAnalysis? @relation(fields: [autoAnalysisId], references: [id])
videos Video[]
}3. AutoAnalysis Model
model AutoAnalysis {
id String @id @default(cuid())
userId String
channelId String
channelName String
channelUrl String
triggerType String // "per_video", "batch_3", "batch_5"
isActive Boolean @default(true)
lastAnalyzedVideoId String?
lastRunAt DateTime?
totalRuns Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relationships
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
analyses Analysis[]
@@unique([userId, channelId])
}4. Video Model
model Video {
id String @id @default(cuid())
analysisId String
youtubeId String
title String
description String? @db.Text
thumbnail String?
duration String?
viewCount Int?
likeCount Int?
commentCount Int?
uploadDate DateTime?
channelName String?
transcript String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relationships
analysis Analysis @relation(fields: [analysisId], references: [id], onDelete: Cascade)
@@index([analysisId])
}Database Relationships
User (1) ──────── (N) Analysis
│ │
│ │
└── (N) AutoAnalysis ─┘
│
└── (N) Analysis
│
└── (N) Video
Authentication and Authorization
Auth.js v5 Configuration (auth.ts)
export const { handlers: { GET, POST }, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
session: { strategy: "jwt" },
pages: {
signIn: "/login",
},
callbacks: {
async session({ token, session }) {
if (token) {
session.user.id = token.id;
session.user.name = token.name;
session.user.email = token.email;
session.user.image = token.picture;
session.user.role = token.role;
}
return session;
},
async jwt({ token }) {
if (!token.sub) return token;
const existingUser = await getUserById(token.sub);
if (!existingUser) return token;
token.name = existingUser.name;
token.email = existingUser.email;
token.picture = existingUser.image;
token.role = existingUser.role;
return token;
},
},
...authConfig,
});Session Management (lib/session.ts)
export async function getCurrentUser(): Promise<User | null> {
const session = await auth();
return session?.user ?? null;
}
export async function requireAuth(): Promise<User> {
const user = await getCurrentUser();
if (!user) {
throw new Error("Authentication required");
}
return user;
}Role-Based Access Control
enum UserRole {
USER = "USER",
ADMIN = "ADMIN"
}
// Middleware for role checking
export function requireRole(role: UserRole) {
return async (req: NextRequest) => {
const user = await getCurrentUser();
if (!user || user.role !== role) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
};
}Background Jobs and Automation
Cron Job System (/api/cron/auto-analysis)
export async function GET() {
// Vercel Cron: runs every 30 minutes
const results = await runAutoAnalysisCron();
return NextResponse.json({ results });
}
async function runAutoAnalysisCron(): Promise<CronResult[]> {
const autoAnalyses = await prisma.autoAnalysis.findMany({
where: { isActive: true },
include: { user: true },
});
for (const aa of autoAnalyses) {
// 1. Get latest videos from YouTube
const videos = await getChannelVideos(aa.channelId, 10);
// 2. Find new videos since last analysis
const newVideos = findNewVideos(videos, aa.lastAnalyzedVideoId);
// 3. Check trigger conditions
if (shouldTriggerAnalysis(newVideos, aa.triggerType)) {
// 4. Validate user credits
if (hasEnoughCredits(aa.user, newVideos.length)) {
// 5. Trigger analysis
await triggerAutoAnalysis(aa, newVideos);
}
}
}
}Analysis Processing Pipeline
1. Data Collection Phase
async function collectAnalysisData(channelId: string, videoCount: number) {
// Parallel data fetching for performance
const [channelInfo, videos] = await Promise.all([
getChannelInfo(channelId),
getChannelVideos(channelId, videoCount)
]);
// Fetch video transcripts in parallel
const transcripts = await Promise.all(
videos.map(video => getVideoTranscript(video.id))
);
return { channelInfo, videos, transcripts };
}2. AI Processing Phase
async function processWithAI(analysisData: AnalysisData, analysisType: string) {
const prompt = buildAnalysisPrompt(analysisData, analysisType);
if (analysisType === 'channel-deep-dive') {
// Include sentiment analysis for premium tier
const sentimentData = await analyzeSentiment(analysisData.videos);
prompt.sentimentData = sentimentData;
}
return await generateAnalysis(prompt, analysisType);
}3. Result Storage Phase
async function storeAnalysisResults(
analysisId: string,
htmlResult: string,
videoData: VideoData[]
) {
await prisma.$transaction(async (tx) => {
// Update analysis with results
await tx.analysis.update({
where: { id: analysisId },
data: {
htmlResult,
status: 'completed',
summary_preview: extractPreview(htmlResult)
}
});
// Store video data
await tx.video.createMany({
data: videoData.map(video => ({
analysisId,
...video
}))
});
// Deduct credits after successful completion
await tx.user.update({
where: { id: userId },
data: {
credits: { decrement: creditCost },
creditsUsed: { increment: creditCost }
}
});
});
}Error Handling and Logging
Global Error Handling
class AnalysisError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 500
) {
super(message);
this.name = 'AnalysisError';
}
}
export function handleApiError(error: unknown): NextResponse {
console.error('API Error:', error);
if (error instanceof AnalysisError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{ status: error.statusCode }
);
}
// Log unexpected errors for monitoring
console.error('Unexpected error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}Request Validation
import { z } from 'zod';
const analysisRequestSchema = z.object({
channelUrl: z.string().url(),
videoCount: z.number().min(1).max(50),
analysisType: z.enum(['standard-summary', 'executive-summary', 'channel-deep-dive']).optional()
});
export function validateAnalysisRequest(data: unknown) {
return analysisRequestSchema.parse(data);
}Performance Optimization
Database Query Optimization
// Efficient analysis fetching with proper includes
export async function getAnalysisWithVideos(analysisId: string, userId: string) {
return await prisma.analysis.findUnique({
where: { id: analysisId },
include: {
videos: {
select: {
id: true,
youtubeId: true,
title: true,
thumbnail: true,
viewCount: true,
likeCount: true,
duration: true,
uploadDate: true,
},
orderBy: { uploadDate: 'desc' }
}
}
});
}
// Paginated analysis history
export async function getUserAnalysisHistory(
userId: string,
page: number = 1,
limit: number = 10
) {
const skip = (page - 1) * limit;
return await prisma.analysis.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
skip,
take: limit,
select: {
id: true,
channelUrl: true,
channel_name: true,
status: true,
createdAt: true,
summary_preview: true,
}
});
}Caching Strategy
// Redis-like caching for YouTube API responses
const cache = new Map<string, { data: any; expires: number }>();
export async function getCachedChannelVideos(channelId: string, maxResults: number) {
const cacheKey = `videos:${channelId}:${maxResults}`;
const cached = cache.get(cacheKey);
if (cached && cached.expires > Date.now()) {
return cached.data;
}
const videos = await getChannelVideos(channelId, maxResults);
// Cache for 30 minutes
cache.set(cacheKey, {
data: videos,
expires: Date.now() + 30 * 60 * 1000
});
return videos;
}Security Measures
Input Sanitization
import DOMPurify from 'isomorphic-dompurify';
export function sanitizeHtmlContent(html: string): string {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'h1', 'h2', 'h3'],
ALLOWED_ATTR: []
});
}Rate Limiting
const rateLimiter = new Map<string, { count: number; resetTime: number }>();
export function checkRateLimit(userId: string, limit: number = 10, windowMs: number = 60000): boolean {
const now = Date.now();
const userLimit = rateLimiter.get(userId);
if (!userLimit || now > userLimit.resetTime) {
rateLimiter.set(userId, { count: 1, resetTime: now + windowMs });
return true;
}
if (userLimit.count >= limit) {
return false;
}
userLimit.count++;
return true;
}Environment Variable Validation
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
YOUTUBE_API_KEY: z.string().min(1),
OPENAI_API_KEY: z.string().min(1),
STRIPE_SECRET_KEY: z.string().min(1),
RESEND_API_KEY: z.string().min(1),
NEXTAUTH_SECRET: z.string().min(1),
});
export const env = envSchema.parse(process.env);Prompt Management Backend & API (2025-06-22)
The backend for prompt management provides CRUD, versioning, and test utilities for LLM prompt templates.
API Endpoints
GET /api/prompts— List all prompt templates (admin only)GET /api/prompts/:id— Get a prompt and all its versionsPOST /api/prompts— Create a new prompt templatePATCH /api/prompts/:id— Edit prompt or add a new versionPOST /api/prompts/:id/test— Render prompt with test data (variables, transcript)
Implementation
- All logic is in
/app/api/prompts/andlib/prompts.ts - Each prompt template is linked to an analysis type (e.g., standard-summary)
- Versioning is handled at the database level; all edits create a new version
- Test utilities allow admins to preview prompt output with sample data
Security
- Only
ADMINusers can access these endpoints (middleware enforced) - All changes are logged and versioned
Integration
- The analysis workflow always uses the latest active prompt version for the selected type
- Prompt templates are injected into the OpenAI request pipeline
Maintenance
- All legacy/duplicate prompt admin code has been removed (2025-06-22)
- Only
/app/api/prompts/andlib/prompts.tsare active for prompt management
This backend structure provides a robust, scalable foundation for the YouTube Analyzer application. The modular design, comprehensive error handling, and performance optimizations ensure the system can handle growth while maintaining reliability and user experience.