Skip to content

Database schema

ER diagram (core entities)

erDiagram
    Tenant ||--o{ User : has
    Tenant ||--o{ Patient : has
    Tenant ||--o{ Department : has
    Department ||--o{ Patient : contains
    Patient ||--o{ Document : has
    Patient ||--o{ Prediction : has
    Patient ||--o{ DicomStudy : has
    User ||--o{ Document : uploads
    User }o--|| Department : belongs
    User ||--o{ ChatSession : has
    ChatSession ||--o{ ChatMessage : contains
    User ||--o{ Progress : tracks
    DicomStudy ||--o{ DicomSeries : contains
    DicomSeries ||--o{ DicomFrame : contains
    Document ||--o{ ParsedData : extracts
    Tenant ||--o{ Subscription : has
    User ||--o| UserPreference : has
    User ||--o| TelegramUser : links

Note

ClinicalCase and Test are scenario/exam catalogs (no required FK to User). The RAG vector store is Chroma (not a Postgres table).

Core tables

tenants

Column Type Description
id INTEGER PK
name VARCHAR Clinic name
subdomain VARCHAR UNIQUE Login subdomain

users

Column Type Description
id INTEGER PK
tenant_id FK Clinic
email VARCHAR UNIQUE
role ENUM RBAC role (student, resident, doctor, …)
department_id FK nullable Department
password_hash VARCHAR bcrypt hash

patients

Column Type Description
id INTEGER PK
tenant_id FK
department_id FK
full_name VARCHAR Encrypted when required
date_of_birth DATE
attending_doctor_id FK nullable

documents

Column Type Description
id INTEGER PK
patient_id FK
file_path VARCHAR Encrypted path
status ENUM uploaded/processing/parsed/failed
document_type VARCHAR

dicom_studies / dicom_series / dicom_frames

DICOM hierarchy: Study → Series → Frame (PNG preview).

predictions

Column Type Description
readmission_risk FLOAT 0–1
complication_risk FLOAT 0–1
risk_level VARCHAR low/medium/high
gpt_explanation TEXT

Learning

chat_sessions (ChatSession)

Column Type Description
id INTEGER PK
public_id UUID Public identifier
user_id FK → users Session owner
title VARCHAR nullable Chat title
language VARCHAR Language (en / ru, …)
created_at / updated_at DATETIME

chat_messages (ChatMessage)

Column Type Description
id INTEGER PK
public_id UUID
session_id FK → chat_sessions Cascade delete
role VARCHAR user / assistant / system
content TEXT Message body
sources JSON nullable RAG sources
created_at DATETIME

clinical_cases (ClinicalCase)

Column Type Description
id INTEGER PK
public_id UUID
title VARCHAR Case title
description TEXT Description
specialty VARCHAR nullable Specialty
difficulty VARCHAR nullable Difficulty
steps JSON nullable Step-based scenario
created_at DATETIME

tests (Test)

Column Type Description
id INTEGER PK
public_id UUID
title VARCHAR Exam / quiz title
description TEXT nullable
category VARCHAR nullable Category
difficulty VARCHAR nullable
questions JSON nullable Questions and options
created_at DATETIME

progress (Progress)

Column Type Description
id INTEGER PK
public_id UUID
user_id FK → users
module VARCHAR tutor / cases / exam (etc.)
score FLOAT nullable Score
time_spent INTEGER nullable Seconds
completed_at DATETIME
extra_data JSON nullable Extra metadata

Migrations

Legacy: SQL/Python files in app/db/migrations/ (001–031) — frozen; applied via run_migrations() after create_all when ALEMBIC_ENABLED=false.

Alembic (recommended for prod): alembic/ directory, baseline 001_baseline. Set ALEMBIC_ENABLED=true — then deploy.sh runs alembic upgrade head (with pg_advisory_lock on PostgreSQL) instead of create_all.

docker compose exec app alembic upgrade head
python scripts/run_alembic_migrate.py

Schema inspection

grep "^class " app/models/*.py

Indexes

  • patients(tenant_id, department_id)
  • documents(patient_id, status)
  • dicom_studies(patient_id, study_uid)
  • chat_sessions(user_id), chat_messages(session_id)
  • progress(user_id, module)

Anonymization (researcher)

In access.py, fields full_name, phone, email are replaced with P-{id} ANON on serialization.