The project uses PostgreSQL as the primary database with Prisma ORM for type-safe database access. The database serves both the landing site and web application through a unified backend API. This document outlines the complete database schema, relationships, and design decisions.
erDiagram
User ||--o{ Activity : creates
User ||--o| Collection : has
User ||--o{ ContactSubmission : submits
User {
string id PK
string clerkId UK
string email UK
string name
string role
datetime createdAt
datetime updatedAt
}
Category ||--o{ Item : contains
Category {
string id PK
string name UK
string slug UK
string description
int displayOrder
}
Item ||--o{ ActivityItem : "used in"
Item ||--o{ CollectionItem : "added to"
Item {
string id PK
string name
string description
string metadata
string image
string categoryId FK
boolean featured
boolean available
int quantity
datetime createdAt
datetime updatedAt
}
Collection ||--o{ CollectionItem : contains
Collection {
string id PK
string userId FK
datetime createdAt
datetime updatedAt
}
CollectionItem {
string id PK
string collectionId FK
string itemId FK
int quantity
datetime addedAt
}
Activity ||--o{ ActivityItem : contains
Activity {
string id PK
string userId FK
string activityNumber UK
string data
string status
string userInfo
string contactInfo
string method
string location
datetime scheduledDate
string notes
datetime createdAt
datetime updatedAt
}
ActivityItem {
string id PK
string activityId FK
string itemId FK
int quantity
string itemData
string totalData
}
ContactSubmission ||--|| User : "optional"
ContactSubmission {
string id PK
string name
string email
string subject
string message
string source
string userId FK "optional"
datetime createdAt
}
NewsletterSubscription {
string id PK
string email UK
boolean active
datetime subscribedAt
datetime unsubscribedAt "optional"
}
// This is your Prisma schema file
// Learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// User model - integrated with Clerk
model User {
id String @id @default(cuid())
clerkId String @unique // Clerk user ID
email String @unique
name String?
role Role @default(CUSTOMER)
orders Order[]
cart Cart?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([clerkId])
@@index([email])
}
// Content category
model Category {
id String @id @default(cuid())
name String @unique
slug String @unique
description String?
displayOrder Int @default(0)
items Item[] // Customize: rename based on your domain
@@index([slug])
}
// Content item model
model Item {
id String @id @default(cuid())
name String
description String?
metadata String? // Customize: use for price, tags, or other domain-specific data
image String
category Category @relation(fields: [categoryId], references: [id])
categoryId String
featured Boolean @default(false)
available Boolean @default(true) // Customize: rename to inStock, published, etc.
quantity Int @default(0) // Customize: use for stock, views, likes, etc.
activityItems ActivityItem[]
collectionItems CollectionItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([categoryId])
@@index([featured])
@@index([name])
}
// User collection
model Collection {
id String @id @default(cuid())
user User @relation(fields: [userId], references: [id])
userId String @unique
items CollectionItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
}
// Collection items
model CollectionItem {
id String @id @default(cuid())
collection Collection @relation(fields: [collectionId], references: [id], onDelete: Cascade)
collectionId String
item Item @relation(fields: [itemId], references: [id])
itemId String
quantity Int // Customize: use for quantity, priority, rating, etc.
addedAt DateTime @default(now())
@@unique([collectionId, itemId])
@@index([collectionId])
@@index([itemId])
}
// Activity model
model Activity {
id String @id @default(cuid())
activityNumber String @unique @default(cuid())
user User @relation(fields: [userId], references: [id])
userId String
items ActivityItem[]
data String? // Customize: use for totals, scores, results, etc.
status ActivityStatus @default(PENDING)
// User information (denormalized for history)
userInfo String // Customize: customerName, participantName, etc.
contactInfo String // Customize: email, phone, etc.
// Additional information
method ProcessMethod @default(ONLINE) // Customize: deliveryMethod, accessMethod, etc.
location String? // Customize: address, venue, etc.
scheduledDate DateTime?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([activityNumber])
@@index([status])
@@index([createdAt])
}
// Activity items
model ActivityItem {
id String @id @default(cuid())
activity Activity @relation(fields: [activityId], references: [id], onDelete: Cascade)
activityId String
item Item @relation(fields: [itemId], references: [id])
itemId String
quantity Int
itemData String? // Customize: unitPrice, score, rating, etc.
totalData String? // Customize: totalPrice, totalScore, etc.
@@index([activityId])
@@index([itemId])
}
// Enums
enum Role {
USER // Customize: CUSTOMER, MEMBER, STUDENT, etc.
ADMIN
}
enum ActivityStatus {
PENDING
PROCESSING
READY // Customize: APPROVED, PUBLISHED, etc.
COMPLETED
CANCELLED
}
enum ProcessMethod {
ONLINE // Customize: PICKUP, DIGITAL, etc.
OFFLINE // Customize: DELIVERY, PHYSICAL, etc.
}
// Landing site specific models
model ContactSubmission {
id String @id @default(cuid())
name String
email String
subject String?
message String
source String @default("landing") // landing, web, admin
// Optional: link to user if they're logged in
user User? @relation(fields: [userId], references: [id])
userId String?
createdAt DateTime @default(now())
@@index([email])
@@index([createdAt])
}
model NewsletterSubscription {
id String @id @default(cuid())
email String @unique
active Boolean @default(true)
subscribedAt DateTime @default(now())
unsubscribedAt DateTime?
@@index([email])
@@index([active])
}- Purpose: Stores user account information synchronized with Clerk
- Key Fields:
clerkId: Unique identifier from Clerk authenticationrole: Determines access level (USER or ADMIN)
- Relationships: One-to-many with Activities, One-to-one with Collection, Optional one-to-many with ContactSubmissions
- Customization: Rename USER role based on your domain (CUSTOMER, MEMBER, etc.)
- Used By: Web application (authentication required)
- Purpose: Content categorization
- Key Fields:
slug: URL-friendly identifierdisplayOrder: Controls category display sequence
- Customization: Define categories based on your domain ([Type A], [Type B], etc.)
- Used By: Both landing site (public display) and web application
- Purpose: Core content information
- Key Fields:
metadata: Flexible field for domain-specific data (price, tags, etc.)featured: Boolean flag for homepage display on both sitesquantity: Multi-purpose field (stock, views, likes, etc.)
- Relationships: Many-to-one with Category
- Customization: Rename to Products, Posts, Assets, etc. based on your domain
- Used By: Both landing site (featured items) and web application (full catalog)
- Purpose: Persistent user selections functionality
- Design Decision: Separate collection items for flexibility
- Unique Constraint: One item per collection to prevent duplicates
- Customization: Use for cart, favorites, playlists, etc.
- Used By: Web application only (requires authentication)
- Purpose: Activity management and history
- Design Decisions:
- Denormalized user info for historical accuracy
- Separate activity items for detailed tracking
- Activity number generation for easy reference
- Customization: Use for orders, bookings, submissions, etc.
- Used By: Web application only (requires authentication)
- Purpose: Store contact form submissions from landing site
- Key Fields:
source: Identifies where the submission came from (landing/web/admin)userId: Optional link to authenticated user
- Privacy: Consider GDPR compliance for storing email/personal data
- Used By: Primarily landing site, but can be used by web app
- Purpose: Manage email newsletter subscriptions
- Key Fields:
active: Boolean for subscription statusunsubscribedAt: Timestamp for opt-out tracking
- Compliance: Must support unsubscribe functionality
- Used By: Landing site footer, web app settings
- User:
clerkId,email- Fast authentication lookups - ContactSubmission:
email,createdAt- Admin dashboard queries - NewsletterSubscription:
email,active- Newsletter management
- Item:
categoryId,featured,name- Efficient filtering and search - Category:
slug- URL-based lookups
- Activity:
userId,activityNumber,status,createdAt- Quick activity queries - Collection/ActivityItem: Foreign keys for join operations
npx prisma migrate dev --name init// prisma/seed.ts
const categories = [
{ name: '[Type A]', slug: 'type-a', displayOrder: 1 },
{ name: '[Type B]', slug: 'type-b', displayOrder: 2 },
{ name: '[Type C]', slug: 'type-c', displayOrder: 3 },
{ name: '[Type D]', slug: 'type-d', displayOrder: 4 },
{ name: '[Type E]', slug: 'type-e', displayOrder: 5 }
];
const items = [
{
name: '[Item Name]',
description: '[Item description]',
metadata: '[domain-specific data]', // price, tags, etc.
categorySlug: 'type-a',
featured: true,
quantity: 20 // stock, views, etc.
},
// ... more items
];metadata,data,itemData,totalData: String fields for domain-specific data (prices, scores, etc.)
- Most string fields: VARCHAR(255) default
- Description fields: TEXT for longer content
- User:
clerkId,email - Category:
name,slug - Activity:
activityNumber - CollectionItem: Composite key
[collectionId, itemId]
- Backup Schedule: Daily automated backups
- Index Optimization: Monthly ANALYZE operations
- Data Archival: Move completed activities > 2 years to archive
- Query performance tracking
- Slow query log analysis
- Connection pool monitoring
- Row-level security via application layer
- Encrypted connections (SSL/TLS)
- Regular security updates
- Audit logging for sensitive operations
-- Featured items for homepage
SELECT * FROM "Item"
WHERE featured = true AND available = true
ORDER BY "updatedAt" DESC
LIMIT 6;
-- Categories for navigation
SELECT name, slug,
(SELECT COUNT(*) FROM "Item" WHERE "categoryId" = c.id) as item_count
FROM "Category" c
ORDER BY "displayOrder";-- User's collection with items
SELECT c.*, ci.*, i.*
FROM "Collection" c
JOIN "CollectionItem" ci ON ci."collectionId" = c.id
JOIN "Item" i ON i.id = ci."itemId"
WHERE c."userId" = ?
ORDER BY ci."addedAt" DESC;- Landing Site Analytics: Page views, conversion tracking
- A/B Testing Tables: Store variant performance
- Item Variants: Different options/configurations
- Real-time Tracking: Live status updates
- User Reviews: Rating and feedback system
- Blog/Content Tables: For landing site content marketing
- FAQ Management: Dynamic FAQ content
- Partitioning for activities and contact_submissions by date
- Read replicas for landing site queries
- Materialized views for featured content
- Caching layer (Redis) for public data
- Consider separate analytics database