// One workspace, one team. No multi-tenancy. generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ---------- Auth.js (Prisma adapter) ---------- model User { id String @id @default(cuid()) name String? email String @unique emailVerified DateTime? image String? role String @default("member") // "admin" | "member" createdAt DateTime @default(now()) accounts Account[] sessions Session[] forms Form[] @relation("FormOwner") responses Response[] viewers FormViewer[] tokens AccessToken[] uploads UploadedFile[] partials PartialResponse[] auditLogs AuditLog[] formVersions FormVersion[] } 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]) } // ---------- Forms ---------- model Form { id String @id @default(cuid()) slug String @unique title String description String? ownerId String owner User @relation("FormOwner", fields: [ownerId], references: [id], onDelete: Cascade) // Field schema stored as JSON: Field[] (see src/lib/types.ts) fields String @default("[]") // Form-level settings JSON: { visibility, thanksMessage, theme } settings String @default("{}") published Boolean @default(false) responses Response[] viewers FormViewer[] uploads UploadedFile[] partials PartialResponse[] events FormEvent[] versions FormVersion[] tags FormTag[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // Immutable snapshot of a form's schema for revert/diff. One row per save. model FormVersion { id String @id @default(cuid()) formId String form Form @relation(fields: [formId], references: [id], onDelete: Cascade) // 1-indexed, monotonically increasing per form. version Int title String description String? fields String settings String // The user who saved this version (may be null if author was deleted). authorId String? author User? @relation(fields: [authorId], references: [id], onDelete: SetNull) createdAt DateTime @default(now()) @@unique([formId, version]) @@index([formId, createdAt]) } // Who, besides owner & admins, can view responses. model FormViewer { formId String userId String form Form @relation(fields: [formId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@id([formId, userId]) } model Response { id String @id @default(cuid()) formId String form Form @relation(fields: [formId], references: [id], onDelete: Cascade) submitterId String? submitter User? @relation(fields: [submitterId], references: [id], onDelete: SetNull) // JSON: Record data String createdAt DateTime @default(now()) } model FormEvent { id String @id @default(cuid()) formId String form Form @relation(fields: [formId], references: [id], onDelete: Cascade) // "view" | "start" | "submit" | "abandon" kind String sessionKey String durationMs Int? createdAt DateTime @default(now()) @@index([formId, createdAt]) @@index([formId, kind]) } model PartialResponse { id String @id @default(cuid()) formId String form Form @relation(fields: [formId], references: [id], onDelete: Cascade) submitterId String? submitter User? @relation(fields: [submitterId], references: [id], onDelete: SetNull) anonKey String? data String updatedAt DateTime @updatedAt createdAt DateTime @default(now()) // Nulls are distinct in Postgres, so multiple rows with one side null // can coexist — that's intended (one row per (form, identity)). @@unique([formId, submitterId]) @@unique([formId, anonKey]) } model UploadedFile { id String @id @default(cuid()) formId String form Form @relation(fields: [formId], references: [id], onDelete: Cascade) responseId String? uploaderId String? uploader User? @relation(fields: [uploaderId], references: [id], onDelete: SetNull) // Storage key — opaque path/key in the configured driver. storageKey String // Storage driver that wrote this object (so we can read it back later // even if the default is later switched). driver String @default("local") name String size Int contentType String createdAt DateTime @default(now()) @@index([formId, createdAt]) } // Outbound webhook delivery record. One row per attempt-chain; deliveries with // remaining retries have status=0 and nextAttemptAt in the future. model WebhookDelivery { id String @id @default(cuid()) formId String responseId String url String @default("") status Int @default(0) attempts Int @default(0) lastError String? // Scheduled time of the next retry. When null, no more retries are pending. nextAttemptAt DateTime? lastAttemptAt DateTime? // The signed JSON body, kept so a worker can retry without re-loading state. payload String @default("") // Secret used for HMAC, kept with the row for retry independence. secret String? createdAt DateTime @default(now()) @@index([formId, createdAt]) @@index([nextAttemptAt]) } // Personal access tokens — used for the MCP endpoint. model AccessToken { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) name String // sha256 of the secret; secret is shown once. tokenHash String @unique prefix String // first 8 chars of the secret, shown in lists lastUsedAt DateTime? createdAt DateTime @default(now()) } // ---------- Tags ---------- model Tag { id String @id @default(cuid()) // Lowercase, unique. Display name preserves the casing of the first use. slug String @unique name String // Optional accent color (hex) for the chip; falls back to theme default. color String? createdAt DateTime @default(now()) forms FormTag[] } model FormTag { formId String tagId String form Form @relation(fields: [formId], references: [id], onDelete: Cascade) tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) @@id([formId, tagId]) @@index([tagId]) } // ---------- Audit log ---------- // Append-only feed of mutations. Visible to admins. model AuditLog { id String @id @default(cuid()) // Who did the thing. Null only for system actions (cron, webhook worker, etc). actorId String? actor User? @relation(fields: [actorId], references: [id], onDelete: SetNull) // Verb in past tense: "form.created", "form.deleted", "member.role_changed", etc. action String // What the action operated on. entityType String entityId String? // Free-form JSON for action-specific details (old/new values, etc.). metadata String @default("{}") createdAt DateTime @default(now()) @@index([createdAt]) @@index([entityType, entityId]) @@index([actorId, createdAt]) }