Database Schema
CheerKeeper uses PostgreSQL with Prisma ORM. This document covers the key data models.
Entity Relationship Overview
Company (tenant)
├── User
│ └── Account (OAuth)
├── Event
│ ├── EventDate
│ ├── EventHall
│ ├── Session
│ ├── Division
│ ├── Level
│ └── EventUnit (Performance)
├── Organization
│ └── Team
└── PdfUpload
└── ExtractionJob
Core Models
Company
Multi-tenant root entity. All data belongs to a company.
model Company {
id String @id @default(cuid())
name String
slug String @unique
plan Plan @default(STARTER)
createdAt DateTime @default(now())
users User[]
events Event[]
organizations Organization[]
}
User
User accounts with role-based permissions.
model User {
id String @id @default(cuid())
email String @unique
name String?
password String? // Hashed, null for OAuth-only
role Role @default(MEMBER)
companyId String
company Company @relation(...)
accounts Account[] // OAuth connections
}
enum Role {
OWNER
ADMIN
EDITOR
MEMBER
}
Event
Competition, tournament, or meet.
model Event {
id String @id @default(cuid())
name String
competitionType String
startDate DateTime
endDate DateTime?
status EventStatus @default(DRAFT)
scheduleVersion Int @default(0)
companyId String
// Location
venue String?
address String?
city String?
state String?
zip String?
company Company @relation(...)
dates EventDate[]
halls EventHall[]
sessions Session[]
eventUnits EventUnit[] // Performances
}
enum EventStatus {
DRAFT
PUBLISHED
ARCHIVED
}
Session
Time blocks within an event.
model Session {
id String @id @default(cuid())
eventId String
sessionNumber Int
date DateTime
hall String?
label String?
event Event @relation(...)
eventUnits EventUnit[]
@@unique([eventId, sessionNumber])
}
EventUnit (Performance)
Individual schedule entries.
model EventUnit {
id String @id @default(cuid())
eventId String
sessionId String?
// Team info
teamName String
organization String?
division String?
level String?
// Timing
performanceTime DateTime
checkInTime DateTime?
warmUpTime DateTime?
duration Int? // Minutes
// Location
hall String?
room String?
// Status
isBreak Boolean @default(false)
isScratch Boolean @default(false)
sortOrder Int @default(0)
event Event @relation(...)
session Session? @relation(onDelete: SetNull)
}
Organization & Team
Programs/gyms and their teams.
model Organization {
id String @id @default(cuid())
name String
companyId String
company Company @relation(...)
teams Team[]
}
model Team {
id String @id @default(cuid())
name String
organizationId String
organization Organization @relation(...)
}
Supporting Models
EventDate
Multi-day event support.
model EventDate {
id String @id @default(cuid())
eventId String
date DateTime
label String?
event Event @relation(...)
}
EventHall
Venues/rooms within an event.
model EventHall {
id String @id @default(cuid())
eventId String
name String
event Event @relation(...)
}
Account (OAuth)
OAuth provider connections (NextAuth).
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
user User @relation(...)
@@unique([provider, providerAccountId])
}
Key Constraints
Unique Constraints
Company.slug- URL-friendly identifierUser.email- One account per emailSession(eventId, sessionNumber)- No duplicate session numbersAccount(provider, providerAccountId)- One OAuth link per provider account
Foreign Key Behaviors
| Relation | On Delete |
|---|---|
| EventUnit.sessionId | SetNull |
| EventUnit.eventId | Cascade |
| Session.eventId | Cascade |
| Team.organizationId | Cascade |
Indexes
Performance-critical indexes:
// Fast event lookups
@@index([companyId, status])
@@index([startDate])
// Fast performance queries
@@index([eventId, sessionId])
@@index([eventId, performanceTime])
@@index([teamName])
// Fast session queries
@@index([eventId, sessionNumber])
Version Control
Schedule Versioning
The scheduleVersion field on Event enables optimistic locking:
- Client reads schedule with version N
- Client sends changes with version N
- Server checks version matches
- If match: apply changes, increment to N+1
- If mismatch: return 409 Conflict
Migrations
Running Migrations
# Development (auto-apply)
npx prisma db push
# Production (migration files)
npx prisma migrate deploy
Creating Migrations
npx prisma migrate dev --name description_of_change
Related Docs
- Architecture - System design
- Schedule Engine - How scheduling works
- Sessions - Session model details