Skip to main content

Data model

The full schema is server/prisma/schema.prisma. This page summarizes the user-facing tables.

User

id            String  @id @default(uuid())
username String @unique
email String? @unique
passwordHash String?
displayName String
isAdmin Boolean @default(false)
systemRole String @default("user") -- "system_admin" | "user"

Relations: personas, apiKeys, sessions, worldStates, annotations, groupMemberships, projectMemberships, ownedProjects, videoAssignments, sharedByMe, sharedWithMe.

systemRole was added in v0.2.0 and is the source of truth for admin checks. isAdmin is still populated for backward compatibility.

Session

id              String   @id
userId String -> User
token String @unique
expiresAt DateTime
ipAddress String?
userAgent String?
lastActivityAt DateTime

LoginAttempt

id         String   @id
username String
ipAddress String?
success Boolean
failedAt DateTime

Drives the brute-force lockout at the login route.

ApiKey

id            String  @id
userId String? -> User (NULL for admin shared-pool keys)
provider String
keyName String
encryptedKey String
keyMask String
isActive Boolean
lastUsed DateTime?
usageCount Int
@@unique([userId, provider])

Persona

id                String   @id
userId String -> User
name String
role String
informationNeed String
details String?
isSystemGenerated Boolean @default(false)
hidden Boolean @default(false)
projectId String? -> Project

Persona.userId is the ownership column for CASL.

Ontology

id              String  @id
personaId String @unique -> Persona
entityTypes Json
eventTypes Json
roleTypes Json
relationTypes Json

WorldState

id                  String   @id
userId String -> User
projectId String? -> Project
entities Json
events Json
times Json
entityCollections Json
eventCollections Json
timeCollections Json
relations Json
@@unique([userId, projectId])

worldLocations is also part of this document (in the JSON payload, not as a separate column).

Each user has at most one personal world state (projectId = NULL) and one world state per project they belong to. The v0.1.x global userId @unique constraint was relaxed in v0.2.0 to the composite unique index above.

Video

id                    String   @id
filename String @unique
path String
duration Float?
frameRate Float?
resolution String?
metadata Json?
localThumbnailPath String?
sourcePlatform String?
platformVideoId String?
metadataSyncStatus String?
lastMetadataSync DateTime?

VideoSummary

id              String  @id
videoId String -> Video
personaId String -> Persona
summary Json
visualAnalysis String?
audioTranscript String?
keyFrames Json?
confidence Float?
transcriptJson Json?
audioLanguage String?
speakerCount Int?
audioModelUsed String?
visualModelUsed String?
fusionStrategy String?
processingTimeAudio Float?
processingTimeVisual Float?
processingTimeFusion Float?
processedAtAudio DateTime?
processedAtVisual DateTime?
processedAtFusion DateTime?
claimsJson Json?
claimsVersion String?
claimsExtractedAt DateTime?
comment String?
createdBy String?
projectId String? -> Project

VideoSummary.createdBy is the ownership column for CASL.

Annotation

id              String   @id
videoId String -> Video
personaId String? -> Persona (NULL for object annotations)
userId String? -> User (legacy; kept for back-compat)
createdByUserId String? -> User (CASL ownership column, v0.2.0)
projectId String? -> Project
type String "type" | "object"
label String typeId or world-object id
linkType String? "entity" | "event" | "time" | "location" | NULL
frames Json ordered keyframe array
confidence Float?
source String "manual" | "tracking" | "detection"

linkType was added by migration 20260429000000_add_annotation_link_type (v0.1.8 / v0.2.1 restamped as 20260505000000_add_annotation_link_type). createdByUserId was added in v0.2.0; the backfill migration 20260415000000_backfill_rbac_ownership populated it from userId on existing rows.

ImportHistory

id              String   @id
filename String
importedBy String? -> User (set in v0.1.8)
importOptions Json
result Json
success Boolean
itemsImported Int
itemsSkipped Int

Claim

id                  String  @id
summaryId String -> VideoSummary
summaryType String "video" | "collection"
text Text
gloss Json
parentClaimId String?
textSpans Json?
claimerType String?
claimerGloss Json?
claimRelation Json?
claimEventId String?
claimTimeId String?
claimLocationId String?
audio Json?
video Json?
metadata Json?
confidence Float?
modelUsed String?
extractionStrategy String?
comment Text?
createdBy String?
projectId String?

Claim.createdBy is the ownership column for CASL.

ClaimRelation

id              String   @id
sourceClaimId String -> Claim
targetClaimId String -> Claim
relationTypeId String
sourceSpans Json?
targetSpans Json?
confidence Float?
notes Text?
createdBy String?

UserGroup

id           String   @id
name String
description String?
slug String @unique
createdBy String

Relations: members (GroupMembership), projects, resourceShares. UserGroup.createdBy is the ownership column for CASL.

GroupMembership

id        String   @id
userId String -> User
groupId String -> UserGroup
role String "group_owner" | "group_admin" | "group_member"
joinedAt DateTime
@@unique([userId, groupId])

Project

id            String   @id
name String
description String?
slug String @unique
ownerUserId String? -> User
ownerGroupId String? -> UserGroup
settings Json @default("{}")
isArchived Boolean @default(false)
createdBy String

Relations: members (ProjectMembership), videoAssignments (ProjectVideoAssignment), personas, worldStates, annotations, videoSummaries, claims. Project.ownerUserId is the ownership column for CASL.

ProjectMembership

id         String   @id
userId String -> User
projectId String -> Project
role String "project_owner" | "project_manager" | "annotator"
| "reviewer" | "viewer"
joinedAt DateTime
@@unique([userId, projectId])

ProjectVideoAssignment

id              String   @id
projectId String -> Project
videoId String -> Video
assignedUserId String? -> User
source String @default("manual") -- "manual" | "rule"
ruleDefinition Json?
assignedBy String?
assignedAt DateTime
@@unique([projectId, videoId])

VideoAssignmentRule

id          String   @id
name String
description String?
conditions Json -- [{field, operator, value}]
targetType String -- "user" | "project" | "group"
targetId String
isActive Boolean @default(true)
createdBy String

ResourceShare

id                 String   @id
resourceType String -- "annotation" | "summary" | "claim"
-- | "persona" | "world_state"
resourceId String
sharedByUserId String -> User
sharedWithUserId String? -> User
sharedWithGroupId String? -> UserGroup
permissionLevel String @default("read_only") -- | "forkable"
expiresAt DateTime?

Exactly one of sharedWithUserId / sharedWithGroupId is set.

RolePermission

id            String   @id
scope String -- "system" | "group" | "project"
role String
resourceType String
action String
ownOnly Boolean @default(false)
@@unique([scope, role, resourceType, action])

The CASL ability builder reads this table per request to compile the user's Ability. See Concepts > RBAC and Reference > RBAC permissions.

Migrations

Migrations live in server/prisma/migrations/. Notable recent ones:

20260128095411_add_modality_metadata_to_claims
20260128153121_change_modality_metadata_to_arrays
20260130011500_add_comment_fields
20260221000000_add_projects_groups_rbac (v0.2.0)
20260310000000_add_annotation_userid
20260415000000_backfill_rbac_ownership (v0.2.0)
20260505000000_add_annotation_link_type (v0.2.1)

Migrations are stable. Never rewrite a landed migration; add a new one. See Project > Stability.