- NextAuth v5 credentials auth with registration/login pages - API key CRUD (create, list, revoke) with secure hashing - Stripe checkout, webhooks, and customer portal integration - Rate limiting per subscription tier - All dashboard API endpoints scoped to authenticated user - Prisma schema: User, Account, Session, ApiKey, plus Stripe fields - Auth middleware protecting dashboard and API routes Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-Claude) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
236 lines
5.1 KiB
Plaintext
236 lines
5.1 KiB
Plaintext
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
// ─── Auth & Billing ────────────────────────────────────────────
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
email String @unique
|
|
passwordHash String
|
|
name String?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
subscription Subscription?
|
|
apiKeys ApiKey[]
|
|
traces Trace[]
|
|
|
|
@@index([email])
|
|
}
|
|
|
|
model ApiKey {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
name String @default("Default")
|
|
keyHash String @unique // SHA-256 hash of the actual key
|
|
keyPrefix String // First 8 chars for display: "al_xxxx..."
|
|
lastUsedAt DateTime?
|
|
|
|
revoked Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([keyHash])
|
|
@@index([userId])
|
|
}
|
|
|
|
model Subscription {
|
|
id String @id @default(cuid())
|
|
userId String @unique
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
tier SubscriptionTier @default(FREE)
|
|
stripeCustomerId String? @unique
|
|
stripeSubscriptionId String? @unique
|
|
stripePriceId String?
|
|
|
|
currentPeriodStart DateTime?
|
|
currentPeriodEnd DateTime?
|
|
|
|
// Usage tracking for the current billing period
|
|
sessionsUsed Int @default(0)
|
|
sessionsLimit Int @default(20) // Free tier: 20/day, paid: per month
|
|
|
|
status SubscriptionStatus @default(ACTIVE)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([stripeCustomerId])
|
|
@@index([stripeSubscriptionId])
|
|
}
|
|
|
|
enum SubscriptionTier {
|
|
FREE // 20 sessions/day
|
|
STARTER // $5/mo — 1,000 sessions/mo
|
|
PRO // $20/mo — 100,000 sessions/mo
|
|
}
|
|
|
|
enum SubscriptionStatus {
|
|
ACTIVE
|
|
PAST_DUE
|
|
CANCELED
|
|
UNPAID
|
|
}
|
|
|
|
// ─── Observability ─────────────────────────────────────────────
|
|
|
|
model Trace {
|
|
id String @id @default(cuid())
|
|
sessionId String?
|
|
name String
|
|
status TraceStatus @default(RUNNING)
|
|
tags String[] @default([])
|
|
metadata Json?
|
|
|
|
// Owner — nullable for backward compat with existing unowned traces
|
|
userId String?
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
|
|
totalCost Float?
|
|
totalTokens Int?
|
|
totalDuration Int?
|
|
|
|
startedAt DateTime @default(now())
|
|
endedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
decisionPoints DecisionPoint[]
|
|
spans Span[]
|
|
events Event[]
|
|
|
|
@@index([sessionId])
|
|
@@index([status])
|
|
@@index([createdAt])
|
|
@@index([name])
|
|
@@index([userId])
|
|
}
|
|
|
|
model DecisionPoint {
|
|
id String @id @default(cuid())
|
|
traceId String
|
|
trace Trace @relation(fields: [traceId], references: [id], onDelete: Cascade)
|
|
|
|
type DecisionType
|
|
reasoning String?
|
|
|
|
chosen Json
|
|
alternatives Json[]
|
|
|
|
contextSnapshot Json?
|
|
|
|
durationMs Int?
|
|
costUsd Float?
|
|
|
|
parentSpanId String?
|
|
span Span? @relation(fields: [parentSpanId], references: [id])
|
|
|
|
timestamp DateTime @default(now())
|
|
|
|
@@index([traceId])
|
|
@@index([type])
|
|
@@index([timestamp])
|
|
}
|
|
|
|
model Span {
|
|
id String @id @default(cuid())
|
|
traceId String
|
|
trace Trace @relation(fields: [traceId], references: [id], onDelete: Cascade)
|
|
|
|
parentSpanId String?
|
|
parentSpan Span? @relation("SpanTree", fields: [parentSpanId], references: [id])
|
|
childSpans Span[] @relation("SpanTree")
|
|
|
|
name String
|
|
type SpanType
|
|
|
|
input Json?
|
|
output Json?
|
|
|
|
tokenCount Int?
|
|
costUsd Float?
|
|
durationMs Int?
|
|
|
|
status SpanStatus @default(RUNNING)
|
|
statusMessage String?
|
|
|
|
startedAt DateTime @default(now())
|
|
endedAt DateTime?
|
|
|
|
metadata Json?
|
|
|
|
decisionPoints DecisionPoint[]
|
|
|
|
@@index([traceId])
|
|
@@index([parentSpanId])
|
|
@@index([type])
|
|
@@index([startedAt])
|
|
}
|
|
|
|
model Event {
|
|
id String @id @default(cuid())
|
|
traceId String
|
|
trace Trace @relation(fields: [traceId], references: [id], onDelete: Cascade)
|
|
|
|
spanId String?
|
|
type EventType
|
|
name String
|
|
|
|
metadata Json?
|
|
|
|
timestamp DateTime @default(now())
|
|
|
|
@@index([traceId])
|
|
@@index([type])
|
|
@@index([timestamp])
|
|
}
|
|
|
|
enum TraceStatus {
|
|
RUNNING
|
|
COMPLETED
|
|
ERROR
|
|
}
|
|
|
|
enum DecisionType {
|
|
TOOL_SELECTION
|
|
ROUTING
|
|
RETRY
|
|
ESCALATION
|
|
MEMORY_RETRIEVAL
|
|
PLANNING
|
|
CUSTOM
|
|
}
|
|
|
|
enum SpanType {
|
|
LLM_CALL
|
|
TOOL_CALL
|
|
MEMORY_OP
|
|
CHAIN
|
|
AGENT
|
|
CUSTOM
|
|
}
|
|
|
|
enum SpanStatus {
|
|
RUNNING
|
|
COMPLETED
|
|
ERROR
|
|
}
|
|
|
|
enum EventType {
|
|
ERROR
|
|
RETRY
|
|
FALLBACK
|
|
CONTEXT_OVERFLOW
|
|
USER_FEEDBACK
|
|
CUSTOM
|
|
}
|