Database - NeonDB & Prisma
PostgreSQL database setup with NeonDB and Prisma ORM
Database Architecture
The application uses NeonDB (serverless PostgreSQL) as the database provider and Prisma as the ORM. This setup provides a scalable, serverless database with type-safe database queries.
Prisma Client Configuration
Client Initialization
import { PrismaClient } from '@prisma/client';
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log:
process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error']
});
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;How It Works:
-
Global Instance Pattern: Prevents multiple Prisma Client instances
- In development, Next.js hot reloads can create multiple instances
globalForPrismastores client on global object- Reuses existing instance if available
- Creates new instance only if needed
-
Logging Configuration: Different log levels per environment
- Development: Logs queries, errors, and warnings
['query', 'error', 'warn']shows all SQL queries for debugging
- Production: Logs errors only
['error']reduces noise and improves performance
- Development: Logs queries, errors, and warnings
-
Global Assignment: Preserves instance across hot reloads
if (process.env.NODE_ENV !== 'production')check ensures only in development- Stores instance on global object for reuse
- Production creates fresh instance on each deployment
Usage:
import { prisma } from '@/lib/prisma';
// Query database
const videos = await prisma.video.findMany({
where: { userId: 'user_123' }
});Database Schema
The schema defines three main models for the viral clips feature:
Video Model
model Video {
id String @id @default(cuid())
userId String // Clerk user ID
youtubeUrl String
s3Key String // youtube-videos/{uuid}/yt
prompt String?
// Response fields
totalClips Int?
videoDuration String?
detectedLanguage String?
s3Path String?
clips Clip[]
exportedClips ExportedClip[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([s3Key])
}Fields:
id: Unique identifier using CUID (collision-resistant unique ID)userId: Links to Clerk authenticated useryoutubeUrl: Original YouTube video URLs3Key: S3 path where video stored:youtube-videos/{uuid}/ytprompt: Optional custom prompt for clip identificationtotalClips: Number of clips identified by AIvideoDuration: Duration of original videodetectedLanguage: Language detected in videos3Path: Full S3 path to video file
Relations:
- One-to-many with
Clip: A video has multiple clips - One-to-many with
ExportedClip: A video has multiple exported clips
Indexes:
userId: Fast queries for user's videoss3Key: Fast lookups by S3 storage key
Timestamps:
createdAt: Auto-set on record creationupdatedAt: Auto-updated on record modification
Clip Model
model Clip {
id String @id @default(cuid())
videoId String
video Video @relation(fields: [videoId], references: [id], onDelete: Cascade)
start String
end String
title String
summary String @db.Text
viralityScore String
relatedTopics String[] // Array of topics
transcript String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([videoId])
@@index([viralityScore])
}Fields:
id: Unique clip identifiervideoId: Foreign key to parent videostart: Clip start timestamp (seconds or MM:SS format)end: Clip end timestamptitle: AI-generated title for clipsummary: Detailed summary of clip content (TEXT type for long content)viralityScore: Score indicating viral potentialrelatedTopics: Array of topic stringstranscript: Full transcript of clip segment (TEXT type)
Relations:
- Many-to-one with
Video: Each clip belongs to one video onDelete: Cascade: Deleting video deletes all its clips
Indexes:
videoId: Fast queries for clips of specific videoviralityScore: Fast sorting/filtering by virality
Data Types:
@db.Text: PostgreSQL TEXT type for unlimited length stringsString[]: Native array support in PostgreSQL
ExportedClip Model
model ExportedClip {
id String @id @default(cuid())
videoId String
video Video @relation(fields: [videoId], references: [id], onDelete: Cascade)
start String // Original clip start time
end String // Original clip end time
s3Key String // S3 path to the exported video clip
aspectRatio String // 1:1, 16:9, 9:16
targetLanguage String? // Target language for translation (null if none)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([videoId])
}Fields:
id: Unique exported clip identifiervideoId: Foreign key to parent videostart: Original clip start time from identificationend: Original clip end time from identifications3Key: S3 path to exported/processed video fileaspectRatio: Export aspect ratio (1:1 for Instagram, 16:9 for YouTube, 9:16 for TikTok/Reels)targetLanguage: Language for subtitle translation (null if no translation)
Relations:
- Many-to-one with
Video: Each exported clip belongs to one video onDelete: Cascade: Deleting video deletes all exported clips
Indexes:
videoId: Fast queries for exported clips of specific video
Prisma Configuration
Schema Configuration
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}Generator:
prisma-client-js: Generates TypeScript Prisma Client- Auto-generates types matching schema
- Provides type-safe query methods
Datasource:
provider = "postgresql": Uses PostgreSQL databaseurl = env("DATABASE_URL"): Connection string from environment variable- NeonDB provides PostgreSQL-compatible database
Common Queries
Create Video Record
const video = await prisma.video.create({
data: {
userId: 'user_123',
youtubeUrl: 'https://youtube.com/watch?v=...',
s3Key: 'youtube-videos/uuid-123/yt',
prompt: 'Find viral moments'
}
});Update Video with Metadata
await prisma.video.update({
where: { id: videoId },
data: {
totalClips: 5,
videoDuration: '300',
detectedLanguage: 'en',
s3Path: 's3://bucket/path/video.mp4'
}
});Create Multiple Clips
await prisma.clip.createMany({
data: clips.map((clip) => ({
videoId: video.id,
start: clip.start.toString(),
end: clip.end.toString(),
title: clip.title,
summary: clip.summary,
viralityScore: clip.virality_score.toString(),
relatedTopics: clip.related_topics,
transcript: clip.transcript
}))
});Query Video with Relations
const video = await prisma.video.findUnique({
where: { id: videoId },
include: {
clips: {
orderBy: { viralityScore: 'desc' }
},
exportedClips: true
}
});Result Structure:
{
id: 'video_123',
userId: 'user_123',
youtubeUrl: 'https://...',
// ... other video fields
clips: [
{ id: 'clip_1', title: '...', viralityScore: '95', ... },
{ id: 'clip_2', title: '...', viralityScore: '87', ... }
],
exportedClips: [
{ id: 'export_1', s3Key: '...', aspectRatio: '9:16', ... }
]
}Filter User's Videos
const userVideos = await prisma.video.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
include: {
_count: {
select: { clips: true, exportedClips: true }
}
}
});Returns videos with counts of clips and exports without loading full relations.
Delete Video (Cascades to Clips)
await prisma.video.delete({
where: { id: videoId }
});Due to onDelete: Cascade, this automatically deletes:
- All associated clips
- All associated exported clips
NeonDB Integration
Connection
NeonDB provides serverless PostgreSQL with:
- Instant Scaling: Scales to zero when inactive
- Branching: Git-like database branches for development
- Connection Pooling: Built-in pooling for serverless functions
- Low Latency: Global edge network
Connection String
DATABASE_URL="postgresql://user:password@ep-name-123456.region.aws.neon.tech/dbname?sslmode=require"Components:
postgresql://: PostgreSQL protocoluser:password: Database credentialsep-name-123456.region.aws.neon.tech: NeonDB endpoint/dbname: Database name?sslmode=require: SSL required for security
Connection Pooling
For serverless functions, use connection pooling:
# Direct connection (for migrations)
DATABASE_URL="postgresql://..."
# Pooled connection (for application)
DATABASE_URL="postgresql://...?pgbouncer=true"Pooling prevents exhausting database connections in serverless environments.
Migrations
Create Migration
npx prisma migrate dev --name add_video_modelCreates migration file in prisma/migrations/ directory.
Apply Migrations (Production)
npx prisma migrate deployApplies pending migrations to production database.
Generate Prisma Client
npx prisma generateRegenerates Prisma Client after schema changes. Required before using new models/fields.
Reset Database (Development)
npx prisma migrate resetDrops database, recreates schema, and re-runs migrations. Warning: Deletes all data.
Type Safety
Prisma generates TypeScript types automatically:
import { Video, Clip, Prisma } from '@prisma/client';
// Type-safe create input
const videoData: Prisma.VideoCreateInput = {
userId: 'user_123',
youtubeUrl: 'https://...',
s3Key: 'path/to/video',
clips: {
create: [
{
start: '0',
end: '30',
title: 'Clip 1',
// ... TypeScript ensures all required fields
}
]
}
};
// Type-safe query result
const video: Video & { clips: Clip[] } = await prisma.video.findUnique({
where: { id: 'video_123' },
include: { clips: true }
});All database operations are fully type-checked at compile time, catching errors before runtime.
Environment Variables
# NeonDB Connection
DATABASE_URL="postgresql://user:password@host.neon.tech/dbname?sslmode=require"
# Optional: Direct URL for migrations
DIRECT_URL="postgresql://user:password@host.neon.tech/dbname?sslmode=require"The connection string should be kept secure and never committed to version control.