// InnungsApp — Prisma Schema // Stack: PostgreSQL + Prisma ORM + better-auth generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ============================================= // BETTER-AUTH TABLES // ============================================= model User { id String @id name String email String @unique emailVerified Boolean @default(false) @map("email_verified") image String? createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // better-auth admin plugin fields role String? banned Boolean? @default(false) banReason String? @map("ban_reason") banExpires DateTime? @map("ban_expires") // Password management mustChangePassword Boolean? @default(false) @map("must_change_password") // App relations sessions Session[] accounts Account[] member Member? userRoles UserRole[] @@map("user") } model Session { id String @id expiresAt DateTime @map("expires_at") token String @unique createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") ipAddress String? @map("ip_address") userAgent String? @map("user_agent") userId String @map("user_id") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("session") } model Account { id String @id accountId String @map("account_id") providerId String @map("provider_id") userId String @map("user_id") user User @relation(fields: [userId], references: [id], onDelete: Cascade) accessToken String? @map("access_token") refreshToken String? @map("refresh_token") idToken String? @map("id_token") accessTokenExpiresAt DateTime? @map("access_token_expires_at") refreshTokenExpiresAt DateTime? @map("refresh_token_expires_at") scope String? password String? createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@map("account") } model Verification { id String @id identifier String value String expiresAt DateTime @map("expires_at") createdAt DateTime? @default(now()) @map("created_at") updatedAt DateTime? @updatedAt @map("updated_at") @@map("verification") } // ============================================= // ORGANIZATIONS // ============================================= model Organization { id String @id @default(uuid()) name String slug String @unique plan String @default("pilot") // pilot | standard | pro | verband logoUrl String? @map("logo_url") primaryColor String @default("#E63946") @map("primary_color") secondaryColor String? @map("secondary_color") contactEmail String? @map("contact_email") avvAccepted Boolean @default(false) @map("avv_accepted") avvAcceptedAt DateTime? @map("avv_accepted_at") landingPageTitle String? @map("landing_page_title") landingPageText String? @map("landing_page_text") landingPageSectionTitle String? @map("landing_page_section_title") landingPageButtonText String? @map("landing_page_button_text") landingPageHeroImage String? @map("landing_page_hero_image") landingPageHeroOverlayOpacity Int? @default(50) @map("landing_page_hero_overlay_opacity") landingPageFeatures Json? @map("landing_page_features") @db.JsonB landingPageFooter Json? @map("landing_page_footer") @db.JsonB appStoreUrl String? @map("app_store_url") playStoreUrl String? @map("play_store_url") aiEnabled Boolean @default(false) @map("ai_enabled") createdAt DateTime @default(now()) @map("created_at") members Member[] userRoles UserRole[] news News[] stellen Stelle[] termine Termin[] @@map("organizations") } // ============================================= // MEMBERS // ============================================= model Member { id String @id @default(uuid()) orgId String @map("org_id") userId String? @unique @map("user_id") // NULL until magic-link clicked name String betrieb String sparte String ort String telefon String? email String status String @default("aktiv") // aktiv | ruhend | ausgetreten istAusbildungsbetrieb Boolean @default(false) @map("ist_ausbildungsbetrieb") seit Int? avatarUrl String? @map("avatar_url") pushToken String? @map("push_token") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) user User? @relation(fields: [userId], references: [id], onDelete: SetNull) newsAuthored News[] @relation("NewsAuthor") stellen Stelle[] terminAnmeldungen TerminAnmeldung[] sentMessages Message[] conversationMembers ConversationMember[] @@index([orgId]) @@index([status]) @@map("members") } // ============================================= // USER ROLES (multi-tenancy) // ============================================= model UserRole { id String @id @default(uuid()) orgId String @map("org_id") userId String @map("user_id") role String // admin | member createdAt DateTime @default(now()) @map("created_at") org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([orgId, userId]) @@map("user_roles") } // ============================================= // NEWS // ============================================= model News { id String @id @default(uuid()) orgId String @map("org_id") authorId String? @map("author_id") title String body String // Markdown kategorie String // Wichtig | Pruefung | Foerderung | Veranstaltung | Allgemein publishedAt DateTime? @map("published_at") // NULL = Entwurf createdAt DateTime @default(now()) @map("created_at") org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) author Member? @relation("NewsAuthor", fields: [authorId], references: [id], onDelete: SetNull) reads NewsRead[] attachments NewsAttachment[] @@index([orgId]) @@index([publishedAt]) @@map("news") } model NewsRead { id String @id @default(uuid()) newsId String @map("news_id") userId String @map("user_id") readAt DateTime @default(now()) @map("read_at") news News @relation(fields: [newsId], references: [id], onDelete: Cascade) @@unique([newsId, userId]) @@map("news_reads") } model NewsAttachment { id String @id @default(uuid()) newsId String @map("news_id") name String storagePath String @map("storage_path") mimeType String? @map("mime_type") sizeBytes Int? @map("size_bytes") createdAt DateTime @default(now()) @map("created_at") news News @relation(fields: [newsId], references: [id], onDelete: Cascade) @@map("news_attachments") } // ============================================= // STELLENANGEBOTE (Lehrlingsbörse) // ============================================= model Stelle { id String @id @default(uuid()) orgId String @map("org_id") memberId String @map("member_id") sparte String stellenAnz Int @default(1) @map("stellen_anz") verguetung String? // "600-800 € / Monat" lehrjahr String? // "1. Lehrjahr" | "beliebig" beschreibung String? kontaktEmail String @map("kontakt_email") kontaktName String? @map("kontakt_name") aktiv Boolean @default(true) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) member Member @relation(fields: [memberId], references: [id], onDelete: Cascade) @@index([orgId]) @@index([aktiv]) @@map("stellen") } // ============================================= // TERMINE // ============================================= model Termin { id String @id @default(uuid()) orgId String @map("org_id") titel String datum DateTime uhrzeit String? // stored as "HH:MM" endeDatum DateTime? @map("ende_datum") endeUhrzeit String? @map("ende_uhrzeit") ort String? adresse String? typ String // Pruefung | Versammlung | Kurs | Event | Sonstiges beschreibung String? maxTeilnehmer Int? @map("max_teilnehmer") // NULL = unbegrenzt createdAt DateTime @default(now()) @map("created_at") org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) anmeldungen TerminAnmeldung[] @@index([orgId]) @@index([datum]) @@map("termine") } model TerminAnmeldung { id String @id @default(uuid()) terminId String @map("termin_id") memberId String @map("member_id") angemeldetAt DateTime @default(now()) @map("angemeldet_at") termin Termin @relation(fields: [terminId], references: [id], onDelete: Cascade) member Member @relation(fields: [memberId], references: [id], onDelete: Cascade) @@unique([terminId, memberId]) @@map("termin_anmeldungen") } // ============================================= // DIREKTNACHRICHTEN (Chat) // ============================================= model Conversation { id String @id @default(uuid()) orgId String @map("org_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") members ConversationMember[] messages Message[] @@index([orgId]) @@map("conversations") } model ConversationMember { id String @id @default(uuid()) conversationId String @map("conversation_id") memberId String @map("member_id") lastReadAt DateTime? @map("last_read_at") conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) member Member @relation(fields: [memberId], references: [id], onDelete: Cascade) @@unique([conversationId, memberId]) @@map("conversation_members") } model Message { id String @id @default(uuid()) conversationId String @map("conversation_id") senderId String @map("sender_id") body String createdAt DateTime @default(now()) @map("created_at") conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) sender Member @relation(fields: [senderId], references: [id], onDelete: Cascade) @@index([conversationId]) @@map("messages") }