@startuml db-orm ' Schema source: Flyway V1–V60 (excl. V37, V43 — intentionally removed) ' Schema as of: V60 (2026-05-06) ' ⚠ This is a versioned snapshot. Update when the schema changes significantly. hide circle skinparam linetype ortho ' ── Auth ── package "Auth" { entity app_users { id : UUID <> -- email : VARCHAR(255) NOT NULL UNIQUE password : VARCHAR(255) NOT NULL first_name : VARCHAR(100) last_name : VARCHAR(100) birth_date : DATE contact : TEXT enabled : BOOLEAN NOT NULL color : VARCHAR(20) NOT NULL notify_on_reply : BOOLEAN NOT NULL notify_on_mention : BOOLEAN NOT NULL created_at : TIMESTAMP } entity user_groups { id : UUID <> -- name : VARCHAR(255) NOT NULL UNIQUE } entity app_users_groups { app_user_id : UUID <> group_id : UUID <> } entity group_permissions { group_id : UUID <> -- permission : VARCHAR(255) } entity password_reset_tokens { id : UUID <> -- app_user_id : UUID <> token : VARCHAR(64) NOT NULL UNIQUE expires_at : TIMESTAMP NOT NULL used : BOOLEAN NOT NULL created_at : TIMESTAMP NOT NULL } entity invite_tokens { id : UUID <> -- code : VARCHAR(10) NOT NULL UNIQUE label : VARCHAR(255) max_uses : INTEGER use_count : INTEGER NOT NULL prefill_first_name : VARCHAR(255) prefill_last_name : VARCHAR(255) prefill_email : VARCHAR(255) expires_at : TIMESTAMP created_by : UUID <> created_at : TIMESTAMP NOT NULL revoked : BOOLEAN NOT NULL } entity invite_token_group_ids { invite_token_id : UUID <> group_id : UUID <> } } ' ── Documents ── package "Documents" { entity documents { id : UUID <> -- title : VARCHAR(255) NOT NULL original_filename : VARCHAR(255) NOT NULL status : VARCHAR(255) NOT NULL file_path : VARCHAR(255) file_hash : VARCHAR(64) summary : TEXT transcription : TEXT meta_date : DATE meta_location : VARCHAR(255) meta_document_location : VARCHAR(255) archive_box : VARCHAR(255) archive_folder : VARCHAR(255) sender_id : UUID <> metadata_complete : BOOLEAN NOT NULL script_type : VARCHAR(30) NOT NULL thumbnail_key : VARCHAR(255) thumbnail_generated_at : TIMESTAMP thumbnail_aspect : VARCHAR(16) page_count : INTEGER search_vector : tsvector <> created_at : TIMESTAMP updated_at : TIMESTAMP } entity document_receivers { document_id : UUID <> person_id : UUID <> } entity document_tags { document_id : UUID <> tag_id : UUID <> } entity document_versions { id : UUID <> -- document_id : UUID <> editor_id : UUID <> editor_name : VARCHAR(200) NOT NULL saved_at : TIMESTAMP NOT NULL snapshot : JSONB NOT NULL changed_fields : JSONB NOT NULL } entity document_annotations { id : UUID <> -- document_id : UUID <> page_number : INTEGER NOT NULL x : DOUBLE PRECISION NOT NULL y : DOUBLE PRECISION NOT NULL width : DOUBLE PRECISION NOT NULL height : DOUBLE PRECISION NOT NULL color : VARCHAR(20) NOT NULL polygon : JSONB file_hash : VARCHAR(64) created_by : UUID <> created_at : TIMESTAMP NOT NULL } entity document_comments { id : UUID <> -- document_id : UUID <> annotation_id : UUID <> block_id : UUID <> parent_id : UUID <> author_id : UUID <> author_name : VARCHAR(200) NOT NULL content : TEXT NOT NULL created_at : TIMESTAMP NOT NULL updated_at : TIMESTAMP NOT NULL } entity document_training_labels { document_id : UUID <> -- label : VARCHAR(50) NOT NULL } entity comment_mentions { comment_id : UUID <> app_user_id : UUID <> } } ' ── Persons ── package "Persons" { entity persons { id : UUID <> -- first_name : VARCHAR(255) last_name : VARCHAR(255) NOT NULL alias : VARCHAR(255) title : VARCHAR(50) person_type : VARCHAR(20) NOT NULL notes : TEXT birth_year : INTEGER death_year : INTEGER family_member : BOOLEAN NOT NULL } entity person_name_aliases { id : UUID <> -- person_id : UUID <> last_name : VARCHAR(255) NOT NULL first_name : VARCHAR(255) type : VARCHAR(50) NOT NULL sort_order : INTEGER NOT NULL created_at : TIMESTAMPTZ } entity person_relationships { id : UUID <> -- person_id : UUID <> related_person_id : UUID <> relation_type : VARCHAR(30) NOT NULL from_year : INTEGER to_year : INTEGER notes : VARCHAR(2000) created_at : TIMESTAMPTZ NOT NULL } } ' ── Tags ── package "Tags" { entity tag { id : UUID <> -- name : VARCHAR(255) NOT NULL UNIQUE parent_id : UUID <> color : VARCHAR(20) } } ' ── Transcription ── package "Transcription" { entity transcription_blocks { id : UUID <> -- annotation_id : UUID <> document_id : UUID <> text : TEXT label : VARCHAR(200) sort_order : INTEGER NOT NULL version : INTEGER NOT NULL source : VARCHAR(10) NOT NULL reviewed : BOOLEAN NOT NULL created_by : UUID <> updated_by : UUID <> created_at : TIMESTAMP NOT NULL updated_at : TIMESTAMP NOT NULL } entity transcription_block_versions { id : UUID <> -- block_id : UUID <> text : TEXT NOT NULL changed_by : UUID <> changed_at : TIMESTAMP NOT NULL } entity transcription_block_mentioned_persons { block_id : UUID <> person_id : UUID NOT NULL -- display_name : VARCHAR(200) NOT NULL } } ' ── OCR ── package "OCR" { entity ocr_jobs { id : UUID <> -- status : VARCHAR(20) NOT NULL total_documents : INT NOT NULL processed_documents : INT NOT NULL error_count : INT NOT NULL skipped_count : INT NOT NULL created_by : UUID progress_message : TEXT created_at : TIMESTAMPTZ NOT NULL updated_at : TIMESTAMPTZ NOT NULL } entity ocr_job_documents { id : UUID <> -- job_id : UUID <> document_id : UUID <> status : VARCHAR(20) NOT NULL error_message : TEXT current_page : INT total_pages : INT created_at : TIMESTAMPTZ NOT NULL updated_at : TIMESTAMPTZ NOT NULL } entity ocr_training_runs { id : UUID <> -- status : VARCHAR(20) NOT NULL block_count : INT NOT NULL document_count : INT NOT NULL model_name : VARCHAR(100) NOT NULL error_message : TEXT triggered_by : UUID <> person_id : UUID <> cer : DOUBLE PRECISION loss : DOUBLE PRECISION accuracy : DOUBLE PRECISION epochs : INT created_at : TIMESTAMPTZ NOT NULL completed_at : TIMESTAMPTZ } entity sender_models { id : UUID <> -- person_id : UUID <> UNIQUE model_path : TEXT NOT NULL accuracy : DOUBLE PRECISION cer : DOUBLE PRECISION corrected_lines_at_training : INT NOT NULL created_at : TIMESTAMPTZ NOT NULL updated_at : TIMESTAMPTZ NOT NULL } } ' ── Supporting ── package "Supporting" { entity notifications { id : UUID <> -- recipient_id : UUID <> type : VARCHAR(32) NOT NULL document_id : UUID reference_id : UUID annotation_id : UUID actor_name : VARCHAR(255) read : BOOLEAN NOT NULL created_at : TIMESTAMP NOT NULL } entity audit_log { id : UUID <> -- happened_at : TIMESTAMPTZ NOT NULL actor_id : UUID <> kind : VARCHAR(50) NOT NULL document_id : UUID <> payload : JSONB } entity geschichten { id : UUID <> -- title : VARCHAR(255) NOT NULL body : TEXT status : VARCHAR(32) NOT NULL author_id : UUID <> created_at : TIMESTAMP NOT NULL updated_at : TIMESTAMP NOT NULL published_at : TIMESTAMP } entity geschichten_persons { geschichte_id : UUID <> person_id : UUID <> } entity geschichten_documents { geschichte_id : UUID <> document_id : UUID <> } } ' Auth relationships app_users_groups }o--|| app_users : app_user_id app_users_groups }o--|| user_groups : group_id group_permissions }o--|| user_groups : group_id password_reset_tokens }o--|| app_users : app_user_id invite_tokens }o--|| app_users : created_by invite_token_group_ids }o--|| invite_tokens : invite_token_id invite_token_group_ids }o--|| user_groups : group_id ' Document relationships documents }o--o| persons : sender_id document_receivers }o--|| documents : document_id document_receivers }o--|| persons : person_id document_tags }o--|| documents : document_id document_tags }o--|| tag : tag_id document_versions }o--|| documents : document_id document_versions }o--o| app_users : editor_id document_annotations }o--|| documents : document_id document_annotations }o--o| app_users : created_by document_comments }o--|| documents : document_id document_comments }o--o| document_annotations : annotation_id document_comments }o--o| transcription_blocks : block_id document_comments }o--o| app_users : author_id document_comments }o--o| document_comments : parent_id document_training_labels }o--|| documents : document_id comment_mentions }o--|| document_comments : comment_id comment_mentions }o--|| app_users : app_user_id ' Person relationships person_name_aliases }o--|| persons : person_id person_relationships }o--|| persons : person_id person_relationships }o--|| persons : related_person_id ' Tag self-reference tag }o--o| tag : parent_id ' Transcription relationships transcription_blocks }o--|| document_annotations : annotation_id transcription_blocks }o--|| documents : document_id transcription_blocks }o--o| app_users : created_by transcription_blocks }o--o| app_users : updated_by transcription_block_versions }o--|| transcription_blocks : block_id transcription_block_versions }o--o| app_users : changed_by transcription_block_mentioned_persons }o--|| transcription_blocks : block_id ' OCR relationships ocr_job_documents }o--|| ocr_jobs : job_id ocr_job_documents }o--|| documents : document_id ocr_training_runs }o--o| app_users : triggered_by ocr_training_runs }o--o| persons : person_id sender_models ||--|| persons : person_id ' Supporting relationships notifications }o--|| app_users : recipient_id audit_log }o--o| app_users : actor_id audit_log }o--o| documents : document_id geschichten }o--o| app_users : author_id geschichten_persons }o--|| geschichten : geschichte_id geschichten_persons }o--|| persons : person_id geschichten_documents }o--|| geschichten : geschichte_id geschichten_documents }o--|| documents : document_id @enduml