Database Design
[!NOTE] This page embeds the current live schema from prisma/schema.prisma to avoid documentation drift.
Full Prisma schema
// Prisma schema file
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
generator client {
provider = "prisma-client-js"
}
model Event {
id String @id @default(cuid())
title String
description String?
slug String @unique
capacity Int?
deadline DateTime?
confirmedCount Int @default(0)
waitlistCount Int @default(0)
organizerEmail String
dashboardToken String @unique
questions Json
createdAt DateTime @default(now())
organizerId String?
organizer User? @relation(fields: [organizerId], references: [id])
eventDate DateTime?
location String?
communityLink String?
imageUrl String?
archived Boolean @default(false)
status String @default("active")
feedbackSent Boolean @default(false)
reminderSent Boolean @default(false)
dataExpired Boolean @default(false)
registrations Registration[]
views EventView[]
feedback AttendeeFeedback[]
unlocks EventUnlock[]
insight EventInsight?
teamAccess TeamMemberEvent[]
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
password String?
createdAt DateTime @default(now())
plan String @default("free")
planStartDate DateTime?
planEndDate DateTime?
isAdmin Boolean @default(false)
billingCycle String?
suspended Boolean @default(false)
paystackCustomerCode String? @unique
paystackSubscriptionCode String? @unique
creditBalance Float @default(0)
username String? @unique
resetToken String?
resetTokenExpiry DateTime?
consentSystemEmails Boolean @default(false)
onboardingCompleted Boolean @default(false)
onboardingSkipped Boolean @default(false)
accounts Account[]
ownedTeam TeamMember[] @relation("OwnedTeam")
memberOf TeamMember[] @relation("MemberOf")
sessions Session[]
events Event[]
onboarding UserOnboarding?
notifications Notification[]
creditTransactions CreditTransaction[]
featureAccess FeatureAccess[]
organizerFeedback OrganizerFeedback[]
reportDownload ReportDownload?
}
model UserOnboarding {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
completedSteps String[] @default([])
usedFeatures String[] @default([])
tutorialCompleted Boolean @default(false)
tutorialSkipped Boolean @default(false)
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
model Registration {
id String @id @default(cuid())
eventId String
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
answers Json
status String
waitlistPosition Int?
registrationNumber Int?
submittedAt DateTime @default(now())
notified Boolean @default(false)
attendeeEmail String?
consentTransactional Boolean @default(false)
consentMarketing Boolean @default(false)
isDuplicate Boolean @default(false)
checkedIn Boolean @default(false)
checkedInAt DateTime?
qrCode String? @unique
confirmationCode String? @unique
source String @default("form")
}
model ErrorLog {
id String @id @default(cuid())
route String
message String
createdAt DateTime @default(now())
}
model Message {
id String @id @default(cuid())
senderName String?
senderEmail String?
eventId String?
eventTitle String?
type String @default("organizer") // organizer | attendee
rating Int?
body String
read Boolean @default(false)
archived Boolean @default(false)
createdAt DateTime @default(now())
}
model Notification {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
type String
message String
eventId String?
read Boolean @default(false)
createdAt DateTime @default(now())
}
model CreditTransaction {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
amount Float
type String
description String
reference String?
eventId String?
createdAt DateTime @default(now())
}
model EventUnlock {
id String @id @default(cuid())
eventId String
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
userId String
feature String
unlockedAt DateTime @default(now())
}
model OrganizerFeedback {
id String @id @default(cuid())
organizerId String
organizer User @relation(fields: [organizerId], references: [id])
type String // "complaint" | "compliment" | "suggestion" | "general"
subject String
message String
rating Int?
status String @default("unread")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model EventView {
id String @id @default(cuid())
eventId String
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
viewedAt DateTime @default(now())
source String?
}
model AttendeeFeedback {
id String @id @default(cuid())
eventId String
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
registrationId String @unique
rating Int
enjoyed String?
improve String?
complaint String?
submittedAt DateTime @default(now())
}
model TeamMember {
id String @id @default(cuid())
ownerId String
owner User @relation("OwnedTeam", fields: [ownerId], references: [id], onDelete: Cascade)
memberId String?
member User? @relation("MemberOf", fields: [memberId], references: [id], onDelete: SetNull)
email String
status String @default("pending")
inviteToken String @unique
createdAt DateTime @default(now())
eventAccess TeamMemberEvent[]
}
model TeamMemberEvent {
id String @id @default(cuid())
teamMemberId String
teamMember TeamMember @relation(fields: [teamMemberId], references: [id], onDelete: Cascade)
eventId String
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([teamMemberId, eventId])
}
model EventInsight {
id String @id @default(cuid())
eventId String @unique
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
cards Json
generatedAt DateTime @default(now())
}
model FeatureAccess {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
eventId String?
feature String
usedAt DateTime?
expiresAt DateTime
createdAt DateTime @default(now())
}
model FeatureInterest {
id String @id @default(cuid())
email String
featureName String
createdAt DateTime @default(now())
@@unique([email, featureName])
}
model ReportDownload {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
downloadsRemaining Int @default(0)
totalPurchased Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ReportDownloadTransaction {
id String @id @default(cuid())
userId String
bundleKey String
amountKsh Int
downloads Int
reference String @unique
createdAt DateTime @default(now())
}
Relationships and foreign keys
Primary relational paths in the schema:
- User.id -> Event.organizerId
- Event.id -> Registration.eventId
- User.id -> Notification.userId
- TeamMember.id -> TeamMemberEvent.teamMemberId
- Event.id -> TeamMemberEvent.eventId
Indexes and constraints
The schema currently relies on strong uniqueness constraints and relation keys, including:
- Event.slug unique
- Event.dashboardToken unique
- Registration.qrCode unique
- Registration.confirmationCode unique
- TeamMember.inviteToken unique
- TeamMemberEvent unique(teamMemberId, eventId)
- FeatureInterest unique(email, featureName)
Audit model note
The prompt referenced AuditLog. In this repository, the operational audit/error model is ErrorLog.
Neon pooling and pgBouncer usage
Runtime and migration URLs are split by design:
- DATABASE_URL: pooled runtime connection
- DIRECT_URL: direct migration connection
Migration workflow
npm run db:generate
npm run db:migrateSeed data workflow
npx prisma db seedIf seed is not configured in your environment, add prisma.seed in package.json or run a custom script in scripts/.