initial project setup

Fastify + Prisma backend, React + Vite frontend, Docker deployment.
Multi-board feedback platform with anonymous cookie auth, passkey
upgrade path, ALTCHA spam protection, plugin system, and full
privacy-first architecture.
This commit is contained in:
2026-03-19 18:05:16 +02:00
commit f07eddf29e
77 changed files with 7031 additions and 0 deletions

View File

@@ -0,0 +1,213 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum AuthMethod {
COOKIE
PASSKEY
}
enum PostType {
FEATURE_REQUEST
BUG_REPORT
}
enum PostStatus {
OPEN
UNDER_REVIEW
PLANNED
IN_PROGRESS
DONE
DECLINED
}
model Board {
id String @id @default(cuid())
slug String @unique
name String
description String?
externalUrl String?
isArchived Boolean @default(false)
voteBudget Int @default(10)
voteBudgetReset String @default("monthly")
lastBudgetReset DateTime?
allowMultiVote Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
activityEvents ActivityEvent[]
pushSubscriptions PushSubscription[]
}
model User {
id String @id @default(cuid())
authMethod AuthMethod @default(COOKIE)
tokenHash String? @unique
username String?
usernameIdx String? @unique
displayName String?
darkMode String @default("system")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
passkeys Passkey[]
posts Post[]
comments Comment[]
reactions Reaction[]
votes Vote[]
pushSubscriptions PushSubscription[]
}
model Passkey {
id String @id @default(cuid())
credentialId String
credentialIdIdx String @unique
credentialPublicKey Bytes
counter BigInt
credentialDeviceType String
credentialBackedUp Boolean
transports String?
userId String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model Post {
id String @id @default(cuid())
type PostType
title String
description Json
status PostStatus @default(OPEN)
category String?
voteCount Int @default(0)
isPinned Boolean @default(false)
boardId String
authorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
statusChanges StatusChange[]
comments Comment[]
votes Vote[]
adminResponses AdminResponse[]
activityEvents ActivityEvent[]
pushSubscriptions PushSubscription[]
}
model StatusChange {
id String @id @default(cuid())
postId String
fromStatus PostStatus
toStatus PostStatus
changedBy String
createdAt DateTime @default(now())
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
}
model Comment {
id String @id @default(cuid())
body String
postId String
authorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
reactions Reaction[]
}
model Reaction {
id String @id @default(cuid())
emoji String
commentId String
userId String
createdAt DateTime @default(now())
comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([commentId, userId, emoji])
}
model Vote {
id String @id @default(cuid())
weight Int @default(1)
postId String
voterId String
budgetPeriod String
createdAt DateTime @default(now())
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
voter User @relation(fields: [voterId], references: [id], onDelete: Cascade)
@@unique([postId, voterId])
}
model AdminUser {
id String @id @default(cuid())
email String @unique
passwordHash String
createdAt DateTime @default(now())
responses AdminResponse[]
}
model AdminResponse {
id String @id @default(cuid())
body String
postId String
adminId String
createdAt DateTime @default(now())
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
admin AdminUser @relation(fields: [adminId], references: [id], onDelete: Cascade)
}
model ActivityEvent {
id String @id @default(cuid())
type String
boardId String
postId String?
metadata Json
createdAt DateTime @default(now())
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
post Post? @relation(fields: [postId], references: [id], onDelete: SetNull)
@@index([boardId, createdAt])
@@index([createdAt])
}
model PushSubscription {
id String @id @default(cuid())
endpoint String
endpointIdx String @unique
keysP256dh String
keysAuth String
userId String
boardId String?
postId String?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
board Board? @relation(fields: [boardId], references: [id], onDelete: Cascade)
post Post? @relation(fields: [postId], references: [id], onDelete: SetNull)
}
model Category {
id String @id @default(cuid())
name String @unique
slug String @unique
createdAt DateTime @default(now())
}