// Prisma schema — the single source of truth for the database structure. // The entity documentation lives in docs/architecture/data-model.md; keep // both in sync when the schema evolves. generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } /// Typed key-value configuration for the instance (registration mode, /// default quotas, legal pages, …). Values are validated with Zod before /// writing; see the InstanceSettings service (issue #19). model InstanceSetting { key String @id value Json updatedAt DateTime @updatedAt @@map("instance_settings") } enum UserStatus { PENDING_VERIFICATION ACTIVE DISABLED } /// Account profile. Login methods live in UserIdentity (OIDC-ready, /// ADR 0007); Site Admin is a user flag, all other roles are grants. model User { id String @id @default(uuid()) username String @unique email String @unique displayName String @map("display_name") locale String @default("en") isSiteAdmin Boolean @default(false) @map("is_site_admin") status UserStatus @default(PENDING_VERIFICATION) emailVerifiedAt DateTime? @map("email_verified_at") createdAt DateTime @default(now()) @map("created_at") lastLoginAt DateTime? @map("last_login_at") identities UserIdentity[] sessions Session[] authTokens AuthToken[] @@map("users") } /// One row per login method. `provider` is "password" today and /// "oidc:" later; `credential` holds the Argon2id hash for /// password identities. model UserIdentity { id String @id @default(uuid()) userId String @map("user_id") provider String subject String credential String? createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, subject]) @@index([userId]) @@map("user_identities") } /// Server-side browser sessions (ADR 0007). `id` is the SHA-256 hash of /// the opaque cookie token — the raw token is never stored. model Session { id String @id userId String @map("user_id") createdAt DateTime @default(now()) @map("created_at") expiresAt DateTime @map("expires_at") lastSeenAt DateTime @default(now()) @map("last_seen_at") userAgent String? @map("user_agent") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([expiresAt]) @@map("sessions") } enum AuthTokenPurpose { EMAIL_VERIFICATION PASSWORD_RESET } /// Single-use, expiring tokens for e-mail flows. Stored hashed; consuming /// sets `consumedAt` so replays are detectable. model AuthToken { id String @id @default(uuid()) tokenHash String @unique @map("token_hash") userId String @map("user_id") purpose AuthTokenPurpose expiresAt DateTime @map("expires_at") consumedAt DateTime? @map("consumed_at") createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, purpose]) @@map("auth_tokens") } /// Fixed-window rate-limit counters (ADR 0002: no Redis). `key` encodes /// scope and subject, e.g. "login:ip:203.0.113.7". model RateLimit { key String @id windowStart DateTime @map("window_start") count Int @default(0) @@map("rate_limits") } enum MailStatus { PENDING SENT FAILED } /// Outbox for reliable e-mail delivery with retry (issue #12). model MailOutbox { id String @id @default(uuid()) toAddress String @map("to_address") subject String textBody String @map("text_body") htmlBody String @map("html_body") status MailStatus @default(PENDING) attempts Int @default(0) nextAttemptAt DateTime @default(now()) @map("next_attempt_at") lastError String? @map("last_error") createdAt DateTime @default(now()) @map("created_at") sentAt DateTime? @map("sent_at") @@index([status, nextAttemptAt]) @@map("mail_outbox") }