echos-ocr/prisma/schema.prisma
Randall Stillwell ab873f7f08 Add multi-workspace support with org switcher and open registration
- Open registration to all users (remove invite-only gate)
- Add domain auto-join: new users matching org allowedDomains get auto-added
- Track active workspace via activeOrgId on User model
- Refactor JWT callback to resolve active org from all memberships
- Add org switch, list, and create-personal API endpoints
- Add workspace-setup page for users without an org
- Build OrgSwitcher dropdown in sidebar header
- Add allowed email domains management to org settings

Made-with: Cursor
2026-04-16 18:08:08 -05:00

383 lines
12 KiB
Text

generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
// ─── Auth.js tables ──────────────────────────────────────────
model User {
id String @id @default(cuid())
email String @unique
emailVerified DateTime?
hashedPassword String?
username String?
displayName String?
avatarUrl String @default("")
role String @default("viewer")
activeOrgId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
sessions Session[]
memberships OrgMember[]
assignedCards ResponseCard[] @relation("AssignedCards")
assignorCards ResponseCard[] @relation("AssignorCards")
reviewedCards ResponseCard[] @relation("ReviewedCards")
activityLogs ActivityLog[]
notifications Notification[]
authentikUid String? @unique
}
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])
}
// ─── Organization / Multi-tenancy ────────────────────────────
model Organization {
id String @id @default(cuid())
name String
slug String @unique
type String @default("church")
timezone String @default("America/Chicago")
settings Json?
allowedDomains String[] @default([])
onboardingComplete Boolean @default(false)
onboardingStep Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
locations Location[]
members OrgMember[]
invitations Invitation[]
apiKeys ApiKey[]
cards ResponseCard[]
jobs ProcessingJob[]
integrations Integration[]
}
model OrgMember {
id String @id @default(cuid())
userId String
organizationId String
role String @default("viewer")
dismissedHints Json?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([userId, organizationId])
}
model Location {
id String @id @default(cuid())
organizationId String
name String
address String?
timezone String?
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
collectionDays CollectionDay[]
cards ResponseCard[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model CollectionDay {
id String @id @default(cuid())
locationId String
name String
description String?
rrule String?
dayOfWeek Int?
timeStart String?
timeEnd String?
isRecurring Boolean @default(true)
date DateTime?
isActive Boolean @default(true)
location Location @relation(fields: [locationId], references: [id], onDelete: Cascade)
cards ResponseCard[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ─── Integrations ────────────────────────────────────────────
model Integration {
id String @id @default(cuid())
organizationId String
provider String
name String
enabled Boolean @default(false)
config Json
fieldMapping Json?
syncDirection String @default("push")
triggerEvents Json?
lastSyncAt DateTime?
lastSyncStatus String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
@@unique([organizationId, provider, name])
@@index([organizationId, provider])
}
// ─── Invitations ─────────────────────────────────────────────
model Invitation {
id String @id @default(cuid())
email String
role String @default("viewer")
organizationId String
invitedById String
token String @unique @default(cuid())
expiresAt DateTime
acceptedAt DateTime?
createdAt DateTime @default(now())
organization Organization @relation(fields: [organizationId], references: [id])
}
// ─── System config ───────────────────────────────────────────
model SystemConfig {
id String @id @default("singleton")
isSetupComplete Boolean @default(false)
setupStep Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ─── Core application models ─────────────────────────────────
model ResponseCard {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String?
gender String?
dateOfBirth String?
maritalStatus String?
maritalStatusOther String?
visitType String?
cellPhone String?
homePhone String?
email String?
address String?
aptNumber String?
city String?
state String?
zip String?
prayerRequests String?
prayerForTeam Boolean @default(false)
prayerConfidential Boolean @default(false)
messageTopics Json?
messageTopicsOther String?
nextStep Json?
attendanceDuration String?
campusPreference Json?
campusPreferenceOther String?
howHeard Json?
howHeardOther String?
serviceAttended String?
followUp String?
notes String?
serviceTime String?
planningCenter String?
iSaidYesBookSent Boolean @default(false)
ftGuestLetterSent Boolean @default(false)
firstTimeGuestDate DateTime?
salvationDate DateTime?
assignedToId String?
assignedById String?
assignedAt DateTime?
reviewedById String?
reviewedAt DateTime?
reviewNotes String?
assignedTo User? @relation("AssignedCards", fields: [assignedToId], references: [id])
assignedBy User? @relation("AssignorCards", fields: [assignedById], references: [id])
reviewedBy User? @relation("ReviewedCards", fields: [reviewedById], references: [id])
sourceFile String?
frontImagePath String?
backImagePath String?
ocrStatus String @default("pending")
reviewStatus String @default("unreviewed")
ocrConfidence Float?
ocrError String?
rawOcrResponse Json?
mondayItemId String?
organizationId String?
locationId String?
collectionDayId String?
collectionDate DateTime?
organization Organization? @relation(fields: [organizationId], references: [id])
location Location? @relation(fields: [locationId], references: [id])
collectionDay CollectionDay? @relation(fields: [collectionDayId], references: [id])
@@index([ocrStatus])
@@index([reviewStatus])
@@index([name])
@@index([createdAt])
@@index([mondayItemId])
@@index([assignedToId])
@@index([reviewedById])
@@index([organizationId])
@@index([locationId])
@@index([collectionDayId])
}
model ProcessingJob {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
fileName String
filePath String
status String @default("queued")
totalPages Int @default(0)
processed Int @default(0)
error String?
cardIds Json?
organizationId String?
organization Organization? @relation(fields: [organizationId], references: [id])
@@index([status])
}
model AppSettings {
id String @id @default("singleton")
ollamaUrl String @default("http://192.168.68.108:11434")
model String @default("llava:7b")
watchDir String @default("")
watching Boolean @default(false)
sourceRetentionDays Int @default(30)
imageRetentionDays Int @default(180)
aiProvider String @default("gateway")
aiModel String @default("")
mondayApiToken String @default("")
mondayBoardId String @default("")
mondayEnabled Boolean @default(false)
mondayColumnMap Json?
mondayWebhookId String @default("")
mondayWebhookUrl String @default("")
webhookUrl String @default("")
webhookSecret String @default("")
webhookEnabled Boolean @default(false)
webhookEvents Json?
emailImapHost String @default("imap.dreamhost.com")
emailImapPort Int @default(993)
emailImapUser String @default("echo-ocr@stillwell.cloud")
emailImapPass String @default("")
emailImapTls Boolean @default(true)
emailFolder String @default("INBOX")
emailWatching Boolean @default(false)
emailProcessed String @default("mark_read")
emailProcessedFolder String @default("Processed")
ftpEnabled Boolean @default(false)
ftpHost String @default("")
ftpPort Int @default(21)
ftpUser String @default("")
ftpPass String @default("")
ftpTls Boolean @default(true)
ftpIncomingDir String @default("/incoming")
ftpProcessedDir String @default("/processed")
}
model ActivityLog {
id String @id @default(cuid())
createdAt DateTime @default(now())
cardId String?
action String
source String
summary String
changes Json?
userId String?
user User? @relation(fields: [userId], references: [id])
@@index([cardId, createdAt])
}
model Notification {
id String @id @default(cuid())
createdAt DateTime @default(now())
read Boolean @default(false)
dismissed Boolean @default(false)
type String
title String
message String
cardId String?
actionUrl String?
meta Json?
userId String?
user User? @relation(fields: [userId], references: [id])
@@index([read, dismissed, createdAt])
@@index([cardId])
@@index([userId])
}
model ApiKey {
id String @id @default(cuid())
name String
hashedKey String @unique
prefix String
organizationId String
permissions Json
lastUsedAt DateTime?
expiresAt DateTime?
createdAt DateTime @default(now())
organization Organization @relation(fields: [organizationId], references: [id])
}