Files
stack/apps/api/prisma/schema.prisma
Jason Woltje 6a038d093b feat(#4): Implement Authentik OIDC authentication with BetterAuth
- Integrated BetterAuth library for modern authentication
- Added Session, Account, and Verification database tables
- Created complete auth module with service, controller, guards, and decorators
- Implemented shared authentication types in @mosaic/shared package
- Added comprehensive test coverage (26 tests passing)
- Documented type sharing strategy for monorepo
- Updated environment configuration with OIDC and JWT settings

Key architectural decisions:
- BetterAuth over Passport.js for better TypeScript support
- Separation of User (DB entity) vs AuthUser (client-safe subset)
- Shared types package to prevent FE/BE drift
- Factory pattern for auth config to use shared Prisma instance

Ready for frontend integration (Issue #6).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Fixes #4
2026-01-28 17:26:34 -06:00

315 lines
10 KiB
Plaintext

// Mosaic Stack Database Schema
// PostgreSQL 17 with pgvector extension
generator client {
provider = "prisma-client-js"
previewFeatures = ["postgresqlExtensions"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
extensions = [pgvector(map: "vector"), uuid_ossp(map: "uuid-ossp")]
}
// ============================================
// ENUMS
// ============================================
enum TaskStatus {
NOT_STARTED
IN_PROGRESS
PAUSED
COMPLETED
ARCHIVED
}
enum TaskPriority {
LOW
MEDIUM
HIGH
}
enum ProjectStatus {
PLANNING
ACTIVE
PAUSED
COMPLETED
ARCHIVED
}
enum WorkspaceMemberRole {
OWNER
ADMIN
MEMBER
GUEST
}
enum ActivityAction {
CREATED
UPDATED
DELETED
COMPLETED
ASSIGNED
COMMENTED
}
enum EntityType {
TASK
EVENT
PROJECT
WORKSPACE
USER
}
// ============================================
// MODELS
// ============================================
model User {
id String @id @default(uuid()) @db.Uuid
email String @unique
name String
emailVerified Boolean @default(false) @map("email_verified")
image String?
authProviderId String? @unique @map("auth_provider_id")
preferences Json @default("{}")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
// Relations
ownedWorkspaces Workspace[] @relation("WorkspaceOwner")
workspaceMemberships WorkspaceMember[]
assignedTasks Task[] @relation("TaskAssignee")
createdTasks Task[] @relation("TaskCreator")
createdEvents Event[] @relation("EventCreator")
createdProjects Project[] @relation("ProjectCreator")
activityLogs ActivityLog[]
sessions Session[]
accounts Account[]
@@map("users")
}
model Workspace {
id String @id @default(uuid()) @db.Uuid
name String
ownerId String @map("owner_id") @db.Uuid
settings Json @default("{}")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
// Relations
owner User @relation("WorkspaceOwner", fields: [ownerId], references: [id], onDelete: Cascade)
members WorkspaceMember[]
tasks Task[]
events Event[]
projects Project[]
activityLogs ActivityLog[]
memoryEmbeddings MemoryEmbedding[]
@@index([ownerId])
@@map("workspaces")
}
model WorkspaceMember {
workspaceId String @map("workspace_id") @db.Uuid
userId String @map("user_id") @db.Uuid
role WorkspaceMemberRole @default(MEMBER)
joinedAt DateTime @default(now()) @map("joined_at") @db.Timestamptz
// Relations
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([workspaceId, userId])
@@index([userId])
@@map("workspace_members")
}
model Task {
id String @id @default(uuid()) @db.Uuid
workspaceId String @map("workspace_id") @db.Uuid
title String
description String? @db.Text
status TaskStatus @default(NOT_STARTED)
priority TaskPriority @default(MEDIUM)
dueDate DateTime? @map("due_date") @db.Timestamptz
assigneeId String? @map("assignee_id") @db.Uuid
creatorId String @map("creator_id") @db.Uuid
projectId String? @map("project_id") @db.Uuid
parentId String? @map("parent_id") @db.Uuid
sortOrder Int @default(0) @map("sort_order")
metadata Json @default("{}")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
completedAt DateTime? @map("completed_at") @db.Timestamptz
// Relations
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
assignee User? @relation("TaskAssignee", fields: [assigneeId], references: [id], onDelete: SetNull)
creator User @relation("TaskCreator", fields: [creatorId], references: [id], onDelete: Cascade)
project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull)
parent Task? @relation("TaskSubtasks", fields: [parentId], references: [id], onDelete: Cascade)
subtasks Task[] @relation("TaskSubtasks")
@@index([workspaceId])
@@index([workspaceId, status])
@@index([workspaceId, dueDate])
@@index([assigneeId])
@@index([projectId])
@@index([parentId])
@@map("tasks")
}
model Event {
id String @id @default(uuid()) @db.Uuid
workspaceId String @map("workspace_id") @db.Uuid
title String
description String? @db.Text
startTime DateTime @map("start_time") @db.Timestamptz
endTime DateTime? @map("end_time") @db.Timestamptz
allDay Boolean @default(false) @map("all_day")
location String?
recurrence Json?
creatorId String @map("creator_id") @db.Uuid
projectId String? @map("project_id") @db.Uuid
metadata Json @default("{}")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
// Relations
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
creator User @relation("EventCreator", fields: [creatorId], references: [id], onDelete: Cascade)
project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull)
@@index([workspaceId])
@@index([workspaceId, startTime])
@@index([creatorId])
@@index([projectId])
@@map("events")
}
model Project {
id String @id @default(uuid()) @db.Uuid
workspaceId String @map("workspace_id") @db.Uuid
name String
description String? @db.Text
status ProjectStatus @default(PLANNING)
startDate DateTime? @map("start_date") @db.Date
endDate DateTime? @map("end_date") @db.Date
creatorId String @map("creator_id") @db.Uuid
color String?
metadata Json @default("{}")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
// Relations
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
creator User @relation("ProjectCreator", fields: [creatorId], references: [id], onDelete: Cascade)
tasks Task[]
events Event[]
@@index([workspaceId])
@@index([workspaceId, status])
@@index([creatorId])
@@map("projects")
}
model ActivityLog {
id String @id @default(uuid()) @db.Uuid
workspaceId String @map("workspace_id") @db.Uuid
userId String @map("user_id") @db.Uuid
action ActivityAction
entityType EntityType @map("entity_type")
entityId String @map("entity_id") @db.Uuid
details Json @default("{}")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
// Relations
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([workspaceId])
@@index([workspaceId, createdAt])
@@index([entityType, entityId])
@@index([userId])
@@map("activity_logs")
}
model MemoryEmbedding {
id String @id @default(uuid()) @db.Uuid
workspaceId String @map("workspace_id") @db.Uuid
content String @db.Text
// Note: vector dimension (1536) must match EMBEDDING_DIMENSION constant in @mosaic/shared
embedding Unsupported("vector(1536)")?
entityType EntityType? @map("entity_type")
entityId String? @map("entity_id") @db.Uuid
metadata Json @default("{}")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
// Relations
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
@@index([workspaceId])
@@map("memory_embeddings")
}
// ============================================
// AUTHENTICATION MODELS (BetterAuth)
// ============================================
model Session {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
token String @unique
expiresAt DateTime @map("expires_at") @db.Timestamptz
ipAddress String? @map("ip_address")
userAgent String? @map("user_agent")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([token])
@@map("sessions")
}
model Account {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
accountId String @map("account_id")
providerId String @map("provider_id")
accessToken String? @map("access_token")
refreshToken String? @map("refresh_token")
idToken String? @map("id_token")
accessTokenExpiresAt DateTime? @map("access_token_expires_at") @db.Timestamptz
refreshTokenExpiresAt DateTime? @map("refresh_token_expires_at") @db.Timestamptz
scope String?
password String?
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([providerId, accountId])
@@index([userId])
@@map("accounts")
}
model Verification {
id String @id @default(uuid()) @db.Uuid
identifier String
value String
expiresAt DateTime @map("expires_at") @db.Timestamptz
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
@@index([identifier])
@@map("verifications")
}