Database
Learn how RankThis uses PostgreSQL with Prisma ORM for data management, migrations, transactions, and key database patterns.
RankThis uses PostgreSQL as the primary database with Prisma ORM for type-safe database operations and automated migrations.
✅ Technology Stack
- • PostgreSQL database
- • Prisma ORM with type safety
- • Automated schema migrations
- • Connection pooling
- • Database transactions
- • Event logging system
🔧 Key Features
- • User and subscription management
- • Authentication tables (NextAuth.js)
- • Subscription event tracking
- • Waitlist management
- • Admin analytics data
- • Audit logging
Database Schema
The database schema includes these primary models:
1// User Management2model User {3 id String @id @default(cuid())4 name String?5 email String @unique6 emailVerified DateTime?7 image String?89 // Relations10 accounts Account[]11 sessions Session[]12 subscription Subscription?1314 createdAt DateTime @default(now())15 updatedAt DateTime @updatedAt1617 @@map(name: "users")18}1920// Subscription Management21model Subscription {22 id String @id @default(cuid())23 userId String @unique24 user User @relation(fields: [userId], references: [id], onDelete: Cascade)2526 // Stripe Integration27 stripeCustomerId String?28 stripeSubscriptionId String? @unique29 stripeSubscriptionItemId String?30 stripePriceId String?31 stripeCurrentPeriodEnd DateTime?3233 // Subscription Status34 status String? // active, canceled, incomplete, etc.35 cancelAtPeriodEnd Boolean @default(false)3637 createdAt DateTime @default(now())38 updatedAt DateTime @updatedAt3940 @@map("subscriptions")41}4243// Event Tracking44model SubscriptionEvent {45 id String @id @default(cuid())46 subscriptionId String47 subscription Subscription @relation(fields: [subscriptionId], references: [id])4849 eventType String // created, updated, canceled, payment_succeeded50 stripeEventId String? @unique51 eventData Json? // Full Stripe event data5253 createdAt DateTime @default(now())5455 @@map("subscription_events")56}
NextAuth.js requires these models for session management:
1// NextAuth.js Required Models2model Account {3 id String @id @default(cuid())4 userId String5 type String6 provider String7 providerAccountId String8 refresh_token String? @db.Text9 access_token String? @db.Text10 expires_at Int?11 token_type String?12 scope String?13 id_token String? @db.Text14 session_state String?1516 user User @relation(fields: [userId], references: [id], onDelete: Cascade)1718 @@unique([provider, providerAccountId])19 @@map("accounts")20}2122model Session {23 id String @id @default(cuid())24 sessionToken String @unique25 userId String26 expires DateTime27 user User @relation(fields: [userId], references: [id], onDelete: Cascade)2829 @@map("sessions")30}3132model VerificationToken {33 identifier String34 token String @unique35 expires DateTime3637 @@unique([identifier, token])38 @@map("verificationtokens")39}
Database Operations
Centralized database connection with connection pooling:
1import { PrismaClient } from "@prisma/client";23const globalForPrisma = globalThis as unknown as {4 prisma: PrismaClient | undefined;5};67export const db = globalForPrisma.prisma ?? new PrismaClient({8 log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],9});1011if (process.env.NODE_ENV !== "production") {12 globalForPrisma.prisma = db;13}
The connection singleton prevents too many connections in development with hot reloading.
Examples of common database operations used throughout the app:
1import { db } from "~/server/db";23// Get user with subscription4export async function getUserWithSubscription(userId: string) {5 return await db.user.findUnique({6 where: { id: userId },7 include: {8 subscription: true9 },10 });11}1213// Create subscription with event logging14export async function createSubscription(data: {15 userId: string;16 stripeCustomerId: string;17 stripeSubscriptionId: string;18 stripePriceId: string;19}) {20 return await db.$transaction(async (tx) => {21 // Create subscription22 const subscription = await tx.subscription.create({23 data: {24 userId: data.userId,25 stripeCustomerId: data.stripeCustomerId,26 stripeSubscriptionId: data.stripeSubscriptionId,27 stripePriceId: data.stripePriceId,28 status: "active",29 },30 });3132 // Log the event33 await tx.subscriptionEvent.create({34 data: {35 subscriptionId: subscription.id,36 eventType: "created",37 eventData: data,38 },39 });4041 return subscription;42 });43}4445// Get subscription analytics46export async function getSubscriptionAnalytics() {47 const [totalSubscriptions, activeSubscriptions, monthlyRevenue] = await Promise.all([48 db.subscription.count(),49 db.subscription.count({50 where: { status: "active" },51 }),52 db.subscription.aggregate({53 where: {54 status: "active",55 stripeCurrentPeriodEnd: {56 gte: new Date(),57 },58 },59 _count: true,60 }),61 ]);6263 return {64 totalSubscriptions,65 activeSubscriptions,66 churnRate: ((totalSubscriptions - activeSubscriptions) / totalSubscriptions) * 100,67 };68}
Schema Migrations
Use these commands for schema changes during development:
# Make schema changes in prisma/schema.prisma, then:# Generate Prisma client (after schema changes)pnpm db:generate# Push schema to development database (no migration file)pnpm db:push# Create a migration file (before committing)pnpm db:migrate dev --name your_migration_name# Reset database (development only)pnpm db:reset
⚠️ Development vs Production
Use db:push for rapid prototyping in development. Always create migrations with db:migrate dev before committing.
Apply migrations to production databases:
# Production migration workflow# 1. Generate migration files locallypnpm db:migrate dev --name add_new_feature# 2. Commit schema.prisma and migration filesgit add prisma/git commit -m "Add new feature migration"# 3. Deploy to production (Vercel/server)pnpm db:migrate deploy# 4. Generate Prisma client for productionpnpm db:generate
Production migrations are automatically applied during deployment if configured in your build pipeline.
Database Transactions
Use transactions for operations that must succeed or fail together:
1import { db } from "~/server/db";23// Subscription cancellation with event logging4export async function cancelSubscription(subscriptionId: string) {5 return await db.$transaction(async (tx) => {6 // Update subscription status7 const subscription = await tx.subscription.update({8 where: { id: subscriptionId },9 data: {10 status: "canceled",11 canceledAt: new Date(),12 cancelAtPeriodEnd: true,13 },14 });1516 // Log cancellation event17 await tx.subscriptionEvent.create({18 data: {19 subscriptionId: subscription.id,20 eventType: "canceled",21 eventData: {22 canceledAt: subscription.canceledAt,23 reason: "user_requested",24 },25 },26 });2728 // Send cancellation email (if needed)29 // await sendCancellationEmail(subscription.userId);3031 return subscription;32 });33}3435// Complex analytics calculation36export async function calculateMRR() {37 return await db.$transaction(async (tx) => {38 const activeSubscriptions = await tx.subscription.findMany({39 where: { status: "active" },40 include: { user: true },41 });4243 const monthlyRevenue = activeSubscriptions.reduce((total, sub) => {44 // Calculate monthly revenue based on plan45 const monthlyAmount = calculateMonthlyAmount(sub.stripePriceId);46 return total + monthlyAmount;47 }, 0);4849 // Cache the result50 await tx.analyticsCache.upsert({51 where: { metric: "mrr" },52 update: {53 value: monthlyRevenue,54 calculatedAt: new Date(),55 },56 create: {57 metric: "mrr",58 value: monthlyRevenue,59 calculatedAt: new Date(),60 },61 });6263 return monthlyRevenue;64 });65}
Event Logging
Track all subscription changes for analytics and debugging:
1// Event logging utility2export async function createSubscriptionEvent(3 subscriptionId: string,4 eventType: string,5 eventData?: any,6 stripeEventId?: string7) {8 return await db.subscriptionEvent.create({9 data: {10 subscriptionId,11 eventType,12 eventData: eventData ? JSON.stringify(eventData) : null,13 stripeEventId,14 },15 });16}1718// Usage in webhook handlers19export async function handleStripeWebhook(event: Stripe.Event) {20 switch (event.type) {21 case "customer.subscription.updated":22 const subscription = event.data.object as Stripe.Subscription;2324 // Update subscription in database25 await db.subscription.update({26 where: { stripeSubscriptionId: subscription.id },27 data: {28 status: subscription.status,29 stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),30 },31 });3233 // Log the event34 await createSubscriptionEvent(35 subscription.id,36 "updated",37 {38 status: subscription.status,39 periodEnd: subscription.current_period_end,40 },41 event.id42 );43 break;44 }45}4647// Analytics query using events48export async function getSubscriptionMetrics(days = 30) {49 const startDate = new Date();50 startDate.setDate(startDate.getDate() - days);5152 const events = await db.subscriptionEvent.groupBy({53 by: ["eventType"],54 where: {55 createdAt: {56 gte: startDate,57 },58 },59 _count: {60 eventType: true,61 },62 });6364 return events.reduce((acc, event) => {65 acc[event.eventType] = event._count.eventType;66 return acc;67 }, {} as Record<string, number>);68}
Development
- •
pnpm db:studio- Open Prisma Studio - •
pnpm db:push- Push schema changes - •
pnpm db:reset- Reset database - •
pnpm db:seed- Run seed script
Production
- •
pnpm db:generate- Generate client - •
pnpm db:migrate- Apply migrations - • Always backup before migrations
- • Test migrations in staging first
Performance
- • Use indexes on frequently queried fields
- • Include relations carefully to avoid N+1
- • Use connection pooling for production
- • Cache expensive analytical queries
Data Integrity
- • Use transactions for multi-step operations
- • Add constraints at database level
- • Log important events for audit trails
- • Validate data before database operations
Database Setup Complete!
Your database layer is configured with type safety and best practices. Next, set up the email system.