Skip to content

Dokumenten-Management — Architektur

Diese Seite beschreibt den technischen Aufbau des Prilog-DMS für Entwickler, technische Administratoren und alle, die das System tiefer verstehen oder erweitern wollen.

Stand: DMS-Phase 0-10 vollständig implementiert (2026-05-03).


Architektur-Überblick

┌─────────────────────────────────────────────────────────────────┐
│                      Web-Client (React)                          │
│                                                                  │
│  ┌─────────┐  ┌──────────────┐  ┌──────────────────────────┐   │
│  │ Sidebar │  │ Liste/Grid/  │  │ Detail-Panel              │   │
│  │ (Filter,│  │ Folders/     │  │ (Vorschau + alle Phase-3- │   │
│  │  Bäume) │  │ Timeline)    │  │  bis Phase-10-Felder)     │   │
│  └─────────┘  └──────────────┘  └──────────────────────────┘   │
└──────────────────────────┬──────────────────────────────────────┘
                           │ REST + JWT
┌──────────────────────────┴──────────────────────────────────────┐
│                     Backend API (Fastify)                         │
│                                                                  │
│  Documents   FolderTrees   DocTypes   Retention   ShareLinks    │
│  Relations   Signatures    Annotations  Templates  EmailAlias    │
│       │           │            │           │           │          │
│       └───────────┴────────────┴───────────┴───────────┘          │
│                            │                                      │
│                       Prisma ORM                                  │
└──────────────────────────┬──────────────────────────────────────┘

       ┌───────────────────┼─────────────────────────────────┐
       │                   │                                  │
┌──────┴──────┐  ┌─────────┴────────┐  ┌──────────────┐  ┌──┴────────────┐
│ PostgreSQL  │  │ Per-Tenant       │  │ Tesseract +  │  │ Stalwart Mail │
│ + tsvector  │  │ MinIO (S3)       │  │ poppler      │  │ + JMAP-Fetch  │
│ + pgvector  │  │ AES-256-GCM      │  │ (OCR)        │  │ (Mail-zu-DMS) │
│   (geplant) │  │                  │  │              │  │               │
└─────────────┘  └──────────────────┘  └──────────────┘  └───────────────┘

Datenmodell

Alle DMS-Modelle liegen im zentralen prisma/schema.prisma. Migrations: 0033_dms_folder_trees bis 0040_dms_annotations_templates_email.

Document (zentrales Modell)

FeldTypBeschreibung
idString (cuid)Eindeutige ID
tenantIdStringTenant-Scope
spaceIdString?Space-Scope (nur bei scope='SPACE')
scopeEnumSPACE oder PERSONAL
ownerUserIdString?Eigentümer (nur bei scope='PERSONAL')
titleStringAnzeigename
descriptionText?Optionale Beschreibung
mimeTypeStringMIME-Typ
sizeBytesIntDateigröße
storageKeyStringS3-Objektpfad
uploadedByStringmatrixUserId des Uploaders (oder system:*)
fileHashString?SHA-256 für Duplikat-Erkennung
contentText?Extrahierter Text für Volltextsuche
searchVectortsvectorVolltextindex (Trigger-aktualisiert)
starredBooleanLesezeichen
lockedBooleanLösch-Sperre
versionIntVersion (1+)
parentIdString?Vorgänger-Version
lastOpenedAtDateTime?Letzter Zugriff
expiresAtDateTime?Manuelles Ablaufdatum
archivedAtDateTime?Archiviert
deletedAtDateTime?Soft-Delete (Papierkorb)
Phase 3
documentTypeIdString?FK auf DocumentType
customFieldsJSON?Werte der Custom-Fields
Phase 5
retentionUntilDateTime?Berechnete Aufbewahrungsfrist
legalHoldBooleanAktiver Legal Hold
legalHoldReasonText?Begründung
legalHoldByString?Wer hat gesetzt
legalHoldAtDateTime?Wann gesetzt
Phase 10
isTemplateBooleanVorlagen-Markierung
templateCategoryString?Kategorie (z.B. "Verträge")

FolderTree + Folder + DocumentFolderPlacement (Phase 1)

Mehrere parallele Hierarchien pro Tenant.

ModellZweck
FolderTreeTop-Level-Hierarchie (Name, Symbol, Reihenfolge)
FolderKnoten im Baum (parentId, treeId)
DocumentFolderPlacementM:N — ein Doc kann in mehreren Ordnern liegen

SavedSearch (Phase 2)

typescript
{ id, tenantId, userId, label, filter: JSON, color?, icon? }

Smart Folders: Filter-Definition wird gespeichert, beim Aufruf serverseitig ausgewertet.

DocumentType + Custom Fields (Phase 3)

typescript
DocumentType {
  id, tenantId, key, label, iconEmoji?, description?,
  fields: JSON,            // Array<{ key, label, type, required, options?, regex? }>
  retentionPolicyId?,      // Default-Aufbewahrung
}

Document.customFields ist eine JSON-Spalte: { field_key: value, ... }. Werte werden gegen DocumentType.fields validiert (Pflicht, Typ, Auswahl).

DocumentRelation (Phase 4)

typescript
{ id, tenantId, fromDocumentId, toDocumentId, relationType, note?, createdAt, createdBy }
relationType ∈ { BELONGS_TO | RELATED_TO | SUPERSEDES | PART_OF }

Bidirektional sichtbar — der API-Endpoint liefert beim Lookup Outgoing und Incoming.

RetentionPolicy (Phase 5)

typescript
RetentionPolicy {
  id, tenantId, name,
  retentionDays: Int,
  actionAfter: 'archive' | 'delete' | 'offer',
  legalHoldOverride: Boolean (default true),
  appliesToTypeId?,        // optional: gilt automatisch für Doc-Typ
}

Daily-Cron läuft durch alle Policies → matched alle fälligen Dokumente → führt Aktion aus (außer offer, das eine AdminTask erzeugt).

typescript
PublicShareLink {
  id, tenantId, documentId, slug (unique),
  passwordHash?,            // bcrypt
  expiresAt?, maxViews?, viewCount,
  revokedAt?, createdBy, createdAt
}
PublicShareView { id, linkId, ip, userAgent, viewedAt }

Public-Endpoint /s/:slug (kein Login). bcrypt-Vergleich, Counter-Increment, Audit-Log.

DocumentSignatureRequest + DocumentSignature (Phase 9)

typescript
DocumentSignatureRequest {
  id, tenantId, documentId, title?, note?,
  status: 'pending' | 'partially_signed' | 'fully_signed' | 'cancelled' | 'expired',
  expiresAt?, cancelledAt?, cancelledBy?, createdBy, createdAt
}
DocumentSignature {
  id, requestId, signerEmail, signerName?,
  status: 'pending' | 'signed' | 'declined',
  inviteSlug (unique),
  invitedAt, signedAt?, declineReason?,
  signerName?, ipAddress?,
  signatureHash?, signatureNonce?, signedDocHash?
}

Signatur-Hash = SHA256(fileHash | signedAt.iso | email | ip | nonce). Reproduzierbar verifizierbar — jede Manipulation am Original wird sichtbar (signedDocHash ≠ aktueller fileHash).

DocumentAnnotation (Phase 10)

typescript
{
  id, documentId, parentId?,    // self-FK für Threads
  authorId, body,
  pageNumber?, posX?, posY?, posWidth?, posHeight?,   // Anchor (optional)
  resolvedAt?, resolvedBy?,
  createdAt, updatedAt
}

Self-FK auf parentId = Threading. resolvedAt setzt einen Thread auf gelöst.

UserDmsEmailAlias (Phase 10)

typescript
{
  id, tenantId, matrixUserId,
  slug (12 char base32),
  fullAddress (unique, dms-<slug>@mail.prilog.chat),
  enabled, createdAt, lastReceivedAt?
}

Eine Adresse pro Nutzer pro Tenant. Wird vom Stalwart-Webhook per fullAddress-Lookup gematcht.


Volltextsuche

Technologie

PostgreSQL tsvector mit deutscher Sprachkonfiguration (german).

Trigger

sql
NEW.search_vector :=
    setweight(to_tsvector('german', coalesce(NEW.title, '')), 'A') ||
    setweight(to_tsvector('german', coalesce(NEW.description, '')), 'B') ||
    setweight(to_tsvector('german', coalesce(NEW.content, '')), 'C');

Treffer im Titel werden höher gewichtet (A > B > C). GIN-Index auf search_vector für O(log n) Lookup.

Suchsyntax (Frontend)

EingabeDB-Query (vereinfacht)
ProtokollPlain prefix
Protokoll KollegiumUND-Verknüpfung (&)
Protprot:* (Prefix-Match)
Protokoll ODER BerichtOR (`
-EntwurfNOT (!)
"Beschluss vom"Phrase (<->)

Die Übersetzung passiert serverseitig im documents-global.router.ts.


Textextraktion

Nach jedem Upload läuft asynchron eine Extraktion:

FormatBibliothek
PDF (mit Text-Layer)pdf-parse
PDF (gescannt)tesseract.js via poppler-utils (pdftoppm)
Word (.docx)mammoth
Excel (.xlsx, .ods)xlsx
PowerPoint (.pptx)officeparser
OpenDocumentofficeparser
RTFrtf-parser
E-Mail (.eml)mailparser
EPUBepub2
Text-Formatenativ UTF-8

Extrahierter Text wird in Document.content gespeichert → Trigger aktualisiert search_vector.

OCR (Phase 7)

Wenn ein PDF kein eingebettetes Text-Layer hat:

  1. pdftoppm rendert jede Seite zu PNG (300 DPI)
  2. tesseract läuft mit deutschen Sprachdaten (-l deu) über jede Seite
  3. Resultierender Text wird in Document.content geschrieben

Tesseract + poppler-utils sind auf api-server systemweit installiert (apt-get install -y tesseract-ocr tesseract-ocr-deu poppler-utils).


Auto-Klassifizierung (Phase 7)

Naive-Bayes-Klassifikator in document-classifier.service.ts:

  1. Tokenisierung (deutsche Stopwords entfernt)
  2. Pro Typ: Wortwahrscheinlichkeiten aus existierenden typisierten Dokumenten (5min cache)
  3. Beim Klassifizieren: argmax(P(typ|text))
  4. Konfidenz = relative Wahrscheinlichkeit
typescript
suggestType(tenantId, content): { typeId, confidence, secondTypeId? }

Voraussetzungen: ≥5 typisierte Dokumente, ≥2 Typen, ≥3 Docs pro Typ.

Auto-Apply-Schwelle: confidence ≥ 0.95 → Typ wird automatisch gesetzt; sonst Vorschlag in customFields._suggestion.

Cache-Invalidation: nach jedem Type-Set wird invalidateClassifierCache(tenantId) gerufen.

Field-Extraction (Regex)

Pro Text-Feld eines Typs kann ein Regex hinterlegt werden. Beim Klassifizieren werden alle Regex-Felder gematcht, leere Doc-Felder werden mit Capture-Group 1 befüllt. Bereits gefüllte Felder werden nicht überschrieben.


Datei-Speicherung (Per-Tenant S3)

Layout

Jeder Tenant hat sein eigenes MinIO-Bucket-Service-Account (TenantRuntime). Schlüssel werden AES-256-GCM verschlüsselt in der DB gehalten.

Space-Dokumente:    spaces/{spaceId}/docs/{uuid}-{name}
Personal:           tenants/{tenantId}/users/{userId}/personal/{uuid}-{name}
Versionen:          spaces/{spaceId}/docs/{uuid}-v{n}-{name}
Vorlage-Kopien:     {scope-prefix}/{uuid}-{name}

Upload-Flow (Presigned)

Browser → POST /upload → { uploadUrl, key }
Browser → PUT  uploadUrl (direkt zu MinIO)
Browser → POST /confirm → DB-Eintrag + Async-Extraction

Der Backend-Server überträgt die Datei selbst nicht — nur Presigned-URLs.


API-Endpunkte (vollständig)

Dokumente

MethodePfadBeschreibung
GET/platform/v1/documentsCross-Space-Suche, Pagination
GET/platform/v1/documents/statsSidebar-Zähler
GET/platform/v1/spaces/:spaceId/documents/:idDetail
POST/platform/v1/spaces/:spaceId/documents/uploadPresigned Upload
POST/platform/v1/spaces/:spaceId/documents/confirm-uploadBestätigung
PATCH/platform/v1/spaces/:spaceId/documents/:idTitle, Description, Tags, Expiry
DELETE/platform/v1/spaces/:spaceId/documents/:idSoft-Delete
GET/platform/v1/spaces/:spaceId/documents/:id/downloadPresigned Download
GET/platform/v1/spaces/:spaceId/documents/:id/versionsVersionshistorie

Folder Trees (Phase 1)

MethodePfad
GET/POST/PATCH/DELETE/platform/v1/folder-trees
GET/POST/PATCH/DELETE/platform/v1/folders
POST/DELETE/platform/v1/documents/:id/folders/:folderId

Saved Searches (Phase 2)

MethodePfad
GET/POST/PATCH/DELETE/platform/v1/saved-searches

Document Types (Phase 3)

MethodePfad
GET/POST/PATCH/DELETE/platform/v1/document-types
PATCH/platform/v1/documents/:id/type

Relations (Phase 4)

MethodePfad
GET/platform/v1/documents/:id/relations
POST/platform/v1/documents/:id/relations
DELETE/platform/v1/documents/:id/relations/:relationId

Retention (Phase 5)

MethodePfad
GET/POST/PATCH/DELETE/platform/v1/retention-policies
POST/platform/v1/documents/:id/legal-hold
DELETE/platform/v1/documents/:id/legal-hold
MethodePfad
GET/POST/DELETE/platform/v1/documents/:id/share-links
GET/api/public/share/:slug
POST/api/public/share/:slug/verify-password

Signatures (Phase 9)

MethodePfad
GET/POST/platform/v1/documents/:id/signature-requests
POST/platform/v1/signature-requests/:id/cancel
GET/platform/v1/signature-requests/:id/certificate (PDF)
GET/api/public/sign/:slug
POST/api/public/sign/:slug/confirm

Annotations (Phase 10)

MethodePfad
GET/POST/platform/v1/documents/:id/annotations
PATCH/DELETE/platform/v1/annotations/:id
POST/platform/v1/annotations/:id/resolve
POST/platform/v1/annotations/:id/reopen

Templates (Phase 10)

MethodePfad
GET/platform/v1/dms-templates
PATCH/platform/v1/documents/:id/template
POST/platform/v1/dms-templates/:id/instantiate

Email Alias (Phase 10)

MethodePfad
GET/platform/v1/dms/email-alias
POST/platform/v1/dms/email-alias/enable
POST/platform/v1/dms/email-alias/disable
POST/platform/v1/dms/email-alias/rotate

Mail-zu-DMS — Stalwart Integration

Eingehende Mail

Stalwart MTA empfängt Mail an dms-xxx@mail.prilog.chat

POST /api/public/inbound/stalwart   (IP-Whitelist Stalwart)

space-email.router iteriert events:
   ├─ Recipient passt zu Space.emailAddress → Space-Email-Flow
   ├─ Recipient passt zu UserDmsEmailAlias.fullAddress → DMS-Personal-Flow
   └─ sonst log + ignore

DMS-Personal-Flow (async):
   1. JMAP-Fetch via fetchEmailByMessageId
   2. Body als .txt → personal Document
   3. Pro Anhang: fetchBlob → personal Document
   4. UserDmsEmailAlias.lastReceivedAt updaten

Speicherort

Mails landen unter tenants/{tenantId}/users/{userId}/personal/.... Document.uploadedBy = 'system:dms-mail'.


Process-Engine DMS-App (Phase 8)

DmsApp im Modul dms-engine mit 5 ComponentKinds:

KindAction
dms.upload-triggerStartet Flow bei Upload (filterbar nach Typ, Space, Tag)
dms.approveAufgabe an Person — onActivate setzt assignedTo via separate UpdateMany
dms.set-typeSetzt documentTypeId + customFields (mit Template-Resolution)
dms.move-to-folderErstellt DocumentFolderPlacement
dms.archiveSetzt archivedAt

Flow-Auto-Trigger

dms-flow-trigger.service.ts → triggerDmsFlowsForDocument(documentId):

  1. Alle Flow-Templates mit DMS-Upload-Trigger prüfen
  2. Template-Match (Filter) → ProcessInstance starten mit inputData.documentId
  3. Idempotent: kein Doppel-Start für gleiches Doc/Template

Hooks: bei Auto-Klassifizierung und manuellem Type-Set.


Frontend-Architektur

src/features/
├── documents/
│   ├── documents-hub.tsx            # 3-Panel Hauptseite
│   ├── documents-panel.tsx          # Space-Sidebar Tab
│   ├── documents-world.tsx          # Welt-Navigation
│   ├── tiptap-viewer.tsx            # Read-only Tiptap
│   └── use-documents.ts             # React Hooks
└── dms/
    ├── folder-trees-view.tsx
    ├── folder-trees-sidebar.tsx
    ├── document-folders-panel.tsx
    ├── saved-searches-sidebar.tsx
    ├── document-type-panel.tsx
    ├── document-types-settings.tsx
    ├── document-relations-panel.tsx
    ├── retention-panel.tsx
    ├── retention-settings.tsx
    ├── share-link-modal.tsx
    ├── signature-modal.tsx
    ├── public-share-page.tsx        # /s/:slug
    ├── public-sign-page.tsx         # /sign/:slug
    ├── document-annotations-panel.tsx
    ├── dms-templates-settings.tsx
    ├── template-picker-modal.tsx
    ├── dms-email-alias-settings.tsx
    └── use-* hooks (1 pro Domain)

Vorschau-System

DateitypRenderer
PDF<iframe> mit Browser-PDF
Bilder<img>
Markdown / TextTiptap (read-only)
OfficeTiptap mit extrahiertem Text

Tiptap mit StarterKit, Tabellen, Code-Highlight, Markdown, Highlight, Typography.


Hintergrund-Jobs

JobAuslöserAufgabe
Text-ExtractionUploadText + OCR + DB-Update
Auto-ClassifierUpload (nach Extract)Typ-Vorschlag, ggf. Auto-Apply
DMS-Flow-TriggerUpload + Type-SetProcessInstances starten
Retention-CronDailyAufbewahrungs-Aktionen
Trash-CleanupDailyPapierkorb >30d endgültig löschen
Re-IndexierungServer-StartDocs ohne content neu indizieren
Mail-zu-DMSStalwart-WebhookMail + Anhänge → Personal Documents

Migrations-Übersicht

MigrationInhalt
0033_dms_folder_treesFolderTree, Folder, DocumentFolderPlacement
0034_dms_tags_saved_searchesSavedSearch + Tag-Verbesserungen
0035_dms_document_typesDocumentType + customFields
0036_dms_retention_policiesRetentionPolicy + Document.retention*/legalHold*
0037_dms_public_share_linksPublicShareLink + PublicShareView
0038_dms_document_relationsDocumentRelation
0039_dms_signaturesDocumentSignatureRequest + DocumentSignature
0040_dms_annotations_templates_emailDocumentAnnotation + Document.isTemplate + UserDmsEmailAlias

Sicherheit & Compliance

AspektUmsetzung
Tenant-IsolationtenantId-Scope in jedem Query, eigene S3-Buckets
S3-VerschlüsselungAES-256-GCM für Service-Account-Keys in DB
DSGVO-LöschungHard-Delete-Endpoint (löscht S3 + DB, bricht legalHold nicht)
AuditPro Doc Activity-Log + Share-Views + Signatur-Trail
Public EndpointsSlug = Geheimnis (24+ char base64url), Rate-Limit, kein Login-Hint
eIDAS Art. 26Hash über Doc + Time + Email + IP + Nonce, PDF-Zertifikat
OCR-ServerLokal auf api-server, kein 3rd-Party
Mail-Webhook-AuthIP-Whitelist + (geplant) HMAC

Geplante Erweiterungen

  • PDF.js Inline-Annotation-Highlights — Datenmodell mit page/pos-Feldern steht (Phase 10), UI fehlt
  • Globale Admin-Übersichten — alle Share-Links, alle Email-Aliases pro Tenant
  • Y.js Multi-User-Editing — Tiptap + Matrix als Transport, Phase 11+
  • Drag & Drop zwischen Spaces — derzeit nur Folder-Drop
  • Erinnerungen vor Ablaufdatum — push/email
  • Embedding-Suche (pgvector) — semantische Suche zusätzlich zu tsvector

Verwandte Themen