generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id @default(cuid()) email String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt sessions Session[] draft RuleDraft? rules PublishedRule[] /// At most one pending verified email change (CR-103). emailChangeToken EmailChangeToken? /// Rules this user was invited to as a stakeholder (after accepting invite). ruleStakeholders RuleStakeholder[] @relation("RuleStakeholderUser") /// Stakeholder rows where this user sent the invite. stakeholderInvitesSent RuleStakeholder[] @relation("RuleStakeholderInvitedBy") } /// Pending email change: user must open verify link sent to `newEmail` (CR-103). /// Separate from `MagicLinkToken` so sign-in and email-change flows cannot be confused. model EmailChangeToken { id String @id @default(cuid()) userId String @unique user User @relation(fields: [userId], references: [id], onDelete: Cascade) newEmail String tokenHash String @unique expiresAt DateTime createdAt DateTime @default(now()) @@index([newEmail]) } model Session { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) tokenHash String @unique expiresAt DateTime createdAt DateTime @default(now()) @@index([userId]) } model MagicLinkToken { id String @id @default(cuid()) email String tokenHash String @unique expiresAt DateTime nextPath String? createdAt DateTime @default(now()) @@index([email]) } model RuleDraft { id String @id @default(cuid()) userId String @unique user User @relation(fields: [userId], references: [id], onDelete: Cascade) payload Json updatedAt DateTime @updatedAt } model PublishedRule { id String @id @default(cuid()) userId String? user User? @relation(fields: [userId], references: [id], onDelete: SetNull) title String summary String? document Json createdAt DateTime @default(now()) updatedAt DateTime @updatedAt stakeholders RuleStakeholder[] @@index([userId]) } /// Invite + access for a published rule: email invite at publish; userId set after magic-link-style accept. model RuleStakeholder { id String @id @default(cuid()) ruleId String rule PublishedRule @relation(fields: [ruleId], references: [id], onDelete: Cascade) /// Normalized lowercase email (invite target). email String /// Publisher at invite time; null if that account was removed. invitedByUserId String? invitedBy User? @relation("RuleStakeholderInvitedBy", fields: [invitedByUserId], references: [id], onDelete: SetNull) /// Set when the invitee completes the verify link (same account as `email`). userId String? user User? @relation("RuleStakeholderUser", fields: [userId], references: [id], onDelete: SetNull) /// One-time invite token (hashed); null after accept or revoke path (consume on verify). inviteTokenHash String? @unique inviteExpiresAt DateTime? invitedAt DateTime @default(now()) acceptedAt DateTime? @@unique([ruleId, email]) @@index([userId]) @@index([email]) } model RuleTemplate { id String @id @default(cuid()) slug String @unique title String category String? description String? body Json sortOrder Int @default(0) featured Boolean @default(false) } /// Recommendation matrix (CR-88). /// JSON in `data/create/customRule/
.json` is canonical; this table is /// rebuilt from those files at `prisma db seed` time so the API can join. /// See `docs/guides/template-recommendation-matrix.md` §7. model MethodFacet { id String @id @default(cuid()) /// One of "communication" | "membership" | "decisionApproaches" | "conflictManagement". section String /// Matches the `id` of an entry in `messages/en/create/customRule/
.json#/methods`. slug String /// One of "size" | "orgType" | "scale" | "maturity". group String /// Canonical facet value id, e.g. "workersCoop", "earlyStage". value String /// `true` iff the JSON marks this method as matching the facet (`✓` cell). matches Boolean /// Optional per-cell weight; reserved for a future weighted-rank pass (v1 ignores). weight Float? @@unique([section, slug, group, value]) @@index([section]) @@index([group, value, matches]) } /// Template-level recommendation matrix (Template Composition, cols G–Y). Canonical /// JSON in `data/templates/templateFacet.json`; rebuilt at seed like /// `MethodFacet`. One row per `(templateSlug, group, value)` where the matrix /// marks a fit (✓). `GET /api/templates?facet.*` joins these rows to user facets. /// See `docs/guides/template-recommendation-matrix.md` (parallel to `MethodFacet` §7). model TemplateFacet { id String @id @default(cuid()) /// `RuleTemplate.slug` (e.g. `consensus`, `do-ocracy`). templateSlug String /// `size` | `orgType` | `scale` | `maturity` — same as `MethodFacet.group`. group String /// Canonical facet value id, e.g. `workersCoop`, `local`. value String /// `true` iff the JSON marks a fit; seed only writes `true` rows. matches Boolean @@unique([templateSlug, group, value]) @@index([templateSlug]) @@index([group, value, matches]) }