Files
crm-system/architecture.md
leocrm-bot 3cc0b2e1b4 phase2: architecture, task_graph v2.1.0, AGENTS.md, quality gates, security review
- architecture.md (2019 lines): 73/73 v1 features, RLS policies, CORS, auth rate limiting, v2 FKs removed
- task_graph.json v2.1.0: 14 tasks (7 v1 + 7 v2), 143 features, 298 ACs, all dict test_specs
- AGENTS.md: 14 tasks mapped, T07a/T07b split, v1/v2 phase plan
- Quality gate reviews: Round 1, 2, 3 (all passed)
- Security review: APPROVED_WITH_CONCERNS (0 critical, 7 major, 8 minor)
- Architecture feasibility review: FEASIBLE_WITH_RISKS (3 critical fixed, 5 major fixed)
- All 3 critical issues from feasibility review resolved
- All pre-implementation security items addressed
2026-06-28 23:07:29 +02:00

81 KiB

LeoCRM — Architecture Document

Scope: This document covers both v1 (Core) and v2 (Plugin) architecture. v1 Features: 73 Core features (F-AUTH, F-COMP, F-CONT, F-CORE, F-DATA, F-UI, F-A11Y, F-SEC, F-INFRA, F-INT, F-MIG, F-NAV, F-SET, F-PLUGIN, F-SCHED, F-SEARCH, F-TEST, F-ENV, F-DOC, F-PERF, F-AI, F-WF) v2 Features: 70 Plugin features (F-CAL, F-DMS, F-FILE, F-FILEUI, F-LINK, F-MAIL, F-PERM, F-TAG) v1 Tasks: T01, T02, T03, T07, T09, T10 v2 Tasks: T04, T05, T06, T11, T08a, T08b, T08c

Projekt: leocrm — Greenfield Architekt: Solution Architect (Agent Zero) Datum: 2026-06-28 Status: Draft — ready for review


1. System Architecture

Overview

LeoCRM ist ein Multi-Tenant CRM mit Plugin-basiertem Erweiterungs-System. Die Architektur folgt API-First-Prinzip: alle Features sind primär über REST API nutzbar, die React SPA ist ein API-Client.

High-Level Diagramm

┌───────────────────────────────────────────────────────┐
│                    Coolify (Docker)                     │
├───────────────────────────────────────────────────────┤
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐ │
│  │  React SPA    │  │  FastAPI     │  │  ARQ Worker  │ │
│  │  (Vite build) │  │  Backend     │  │  (async jobs)│ │
│  │  :80          │  │  :8000       │  │  (background)│ │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘ │
│         │                  │                  │         │
│         │   ┌──────────────┼──────────────┐   │         │
│         │   │  PostgreSQL 16  │  Redis   │   │         │
│         │   │  (data + FTS)    │ (cache+  │   │         │
│         │   │  :5432           │  queue)  │   │         │
│         │   └─────────────────┴──────────┘   │         │
│         │                                    │         │
│  ┌──────┴─────────────────────────────────────┴──────┐ │
│  │  OnlyOffice Document Server :8080 (optional)      │ │
│  └───────────────────────────────────────────────────┘ │
│  ┌───────────────────────────────────────────────────┐ │
│  │  Storage Volume (/data/leocrm)                    │ │
│  │  S3-kompatibel oder lokales Volume               │ │
│  └───────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────┘

Services (Docker Compose)

Service Container Port Purpose
backend FastAPI + Uvicorn 8000 REST API, Session-Auth, OpenAPI
frontend Nginx + React SPA (static) 80 React SPA, served as static files
postgres PostgreSQL 16 5432 Primary database, Full-Text-Search
redis Redis 7 6379 Cache, Session-Store, Job-Queue
worker ARQ Worker (Python) Background jobs (export, mail-sync, reminders)
onlyoffice OnlyOffice Document Server 8080 Inline Office-Editing (optional)

Backend Architecture

backend/
├── app/
│   ├── main.py                 — FastAPI app, lifespan, middleware
│   ├── config.py               — Pydantic Settings (env vars)
│   ├── deps.py                 — Dependency injection (auth, db, tenant)
│   ├── core/                   — Core infrastructure
│   │   ├── db/                 — SQLAlchemy engine, session, base model
│   │   ├── tenant.py           — Tenant-scoping middleware/query filter
│   │   ├── auth.py             — Session auth, password hashing, RBAC
│   │   ├── event_bus.py        — Event publishing/subscribing
│   │   ├── service_container.py — DI container for services
│   │   ├── storage.py          — File storage backend (S3/local)
│   │   ├── cache.py            — Redis cache wrapper
│   │   ├── jobs.py             — ARQ job queue integration
│   │   ├── notifications.py    — Notification service
│   │   └── audit.py            — Audit log middleware
│   ├── models/                 — SQLAlchemy models (one file per module)
│   ├── schemas/                — Pydantic schemas (one file per module)
│   ├── services/               — Business logic (one file per module)
│   ├── routes/                  — FastAPI routers (one file per module)
│   ├── plugins/                — Plugin system
│   │   ├── registry.py         — Plugin discovery, registration
│   │   ├── manifest.py         — Plugin manifest schema
│   │   ├── lifecycle.py        — Install/activate/deactivate/uninstall
│   │   ├── migrations.py       — Plugin DB migration runner
│   │   ├── ui_registry.py      — Plugin UI component registration
│   │   └── builtins/           — Built-in plugins
│   │       ├── dms/            — DMS plugin
│   │       ├── calendar/       — Calendar plugin
│   │       ├── mail/           — Mail plugin
│   │       └── tags/           — Tags plugin
│   ├── workflows/              — Workflow engine
│   │   ├── engine.py           — Workflow execution engine
│   │   ├── code/               — Code-based core workflows
│   │   └── models.py           — Workflow SQLAlchemy models
│   ├── ai/                     — KI-Copilot integration
│   │   ├── copilot.py          — Copilot query/execute logic
│   │   ├── llm_client.py       — LLM API client (OpenAI/Anthropic)
│   │   └── models.py           — ai_conversations model
│   └── utils/                  — Shared utilities (validation, export, import)
├── tests/                     — pytest + httpx
├── alembic/                    — DB migrations
├── pyproject.toml
└── Dockerfile

Frontend Architecture

frontend/
├── src/
│   ├── main.tsx               — React entry point
│   ├── App.tsx                — Root component, router, providers
│   ├── api/                   — API client (axios/fetch), interceptors
│   ├── components/            — Shared UI components
│   │   ├── layout/            — Shell, Sidebar, TopBar, ContentArea
│   │   ├── ui/               — Button, Input, Select, Modal, Toast, Table
│   │   └── shared/           — EmptyState, LoadingState, ConfirmDialog, Pagination
│   ├── features/              — Feature modules
│   │   ├── auth/             — Login, PasswordReset, UserManagement
│   │   ├── companies/        — CompanyList, CompanyDetail, CompanyForm
│   │   ├── contacts/         — ContactList, ContactDetail, ContactForm
│   │   ├── settings/         — SettingsTree, ProfileSettings, RoleEditor
│   │   ├── audit/            — AuditLog
│   │   ├── dashboard/        — Dashboard
│   │   └── search/           — GlobalSearch
│   ├── plugins/              — Plugin UI loading framework
│   │   ├── PluginRegistry.tsx — Plugin UI component registry
│   │   └── PluginLoader.tsx   — Dynamic plugin component loading
│   ├── hooks/                 — Custom React hooks
│   ├── store/                 — Zustand stores
│   ├── i18n/                  — react-i18next setup + locale files
│   ├── styles/                — Global CSS, design tokens, accessibility
│   └── utils/                  — Utilities (format, validation, export)
├── public/
├── index.html
├── vite.config.ts
├── package.json
├── tsconfig.json
└── Dockerfile

2. DB Schema

Design Principles (F-DATA-03, F-DATA-04, F-DATA-06)

v1 Core Features:

  • F-DATA-03: Daten-Validierung — Pydantic schemas validate all API inputs
  • F-DATA-04: PostgreSQL als Datenbank — PostgreSQL 16 with Row-Level Security for tenant isolation
  • F-DATA-06: ARIA-Rollen auf DataTable — frontend tables use ARIA roles for accessibility
  • Multi-Tenant: Jede Tabelle hat tenant_id (UUID). ORM filtert automatisch.
  • Soft-Delete: deleted_at TIMESTAMP NULL auf Companies, Contacts, Folders, Files.
  • Audit Trail: created_at, updated_at, created_by, updated_by auf alle Core-Tabellen.
  • UUID Primary Keys: Alle IDs sind UUID (gen_random_uuid() default).
  • Timestamps: TIMESTAMPTZ für alle Datumsfelder.

Core Tables

tenants

Column Type Constraints
id UUID PK, default gen_random_uuid()
name VARCHAR(200) NOT NULL
slug VARCHAR(100) UNIQUE, NOT NULL
created_at TIMESTAMPTZ NOT NULL, default NOW()
updated_at TIMESTAMPTZ NOT NULL, default NOW()

users

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id, NOT NULL
email VARCHAR(255) NOT NULL
name VARCHAR(200) NOT NULL
password_hash VARCHAR(255) NOT NULL (bcrypt cost=12)
role VARCHAR(50) NOT NULL, default 'viewer'
is_active BOOLEAN default true
preferences JSONB default '{}'
default_calendar_id UUID NULL (v2: FK→calendars.id, added in v2 migration)
default_mail_account_id UUID NULL (v2: FK→mail_accounts.id, added in v2 migration)
created_at TIMESTAMPTZ NOT NULL
updated_at TIMESTAMPTZ NOT NULL

Unique: (tenant_id, email)

user_tenants (N:M — User kann mehreren Tenant angehören)

Column Type Constraints
user_id UUID FK→users.id
tenant_id UUID FK→tenants.id
is_default BOOLEAN default false

PK: (user_id, tenant_id)

roles

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
name VARCHAR(100) NOT NULL
permissions JSONB NOT NULL (module→action→read/write/delete/admin)
field_permissions JSONB default '{}' (field→read/write/hidden)
created_at TIMESTAMPTZ NOT NULL

sessions (Audit Trail — primary session store is Redis)

Column Type Constraints
id UUID PK
user_id UUID FK→users.id
tenant_id UUID FK→tenants.id (active tenant)
csrf_token VARCHAR(255) NOT NULL
expires_at TIMESTAMPTZ NOT NULL
created_at TIMESTAMPTZ NOT NULL

Note: Session lookup at runtime uses Redis (session:{id} with TTL=8h). This PostgreSQL table is an immutable audit trail of all sessions ever created, used for security analysis and forensic logging. Session invalidation deletes the Redis key; the PostgreSQL record persists.

companies

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id, NOT NULL
name VARCHAR(100) NOT NULL
account_number VARCHAR(40) NULL
industry VARCHAR(50) NULL (picklist)
account_type VARCHAR(50) NULL (picklist)
ownership VARCHAR(50) NULL (picklist)
employees INTEGER NULL
annual_revenue DECIMAL(15,2) NULL
phone VARCHAR(30) NULL
fax VARCHAR(30) NULL
email VARCHAR(255) NULL
website VARCHAR(500) NULL
rating VARCHAR(20) NULL (Hot/Warm/Cold)
parent_account_id UUID FK→companies.id NULL (self-ref)
billing_street VARCHAR(250) NULL
billing_city VARCHAR(100) NULL
billing_state VARCHAR(100) NULL
billing_postal_code VARCHAR(20) NULL
billing_country VARCHAR(100) NULL
shipping_street VARCHAR(250) NULL
shipping_city VARCHAR(100) NULL
shipping_state VARCHAR(100) NULL
shipping_postal_code VARCHAR(20) NULL
shipping_country VARCHAR(100) NULL
description TEXT NULL
sic_code VARCHAR(10) NULL
ticker_symbol VARCHAR(30) NULL
account_site VARCHAR(80) NULL
deleted_at TIMESTAMPTZ NULL (soft-delete)
created_at TIMESTAMPTZ NOT NULL
updated_at TIMESTAMPTZ NOT NULL
created_by UUID FK→users.id
updated_by UUID FK→users.id

Index: (tenant_id), (tenant_id, deleted_at), (tenant_id, name), (tenant_id, industry), (tenant_id, billing_country), (tenant_id, rating)

contacts

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id, NOT NULL
first_name VARCHAR(50) NULL
last_name VARCHAR(50) NOT NULL
salutation VARCHAR(20) NULL
email VARCHAR(255) NULL
secondary_email VARCHAR(255) NULL
phone VARCHAR(30) NULL
mobile VARCHAR(30) NULL
home_phone VARCHAR(30) NULL
fax VARCHAR(30) NULL
title VARCHAR(100) NULL
department VARCHAR(100) NULL
reports_to UUID FK→contacts.id NULL
date_of_birth DATE NULL
assistant VARCHAR(50) NULL
assistant_phone VARCHAR(30) NULL
mailing_street VARCHAR(250) NULL
mailing_city VARCHAR(100) NULL
mailing_state VARCHAR(100) NULL
mailing_postal_code VARCHAR(20) NULL
mailing_country VARCHAR(100) NULL
other_street VARCHAR(250) NULL
other_city VARCHAR(100) NULL
other_state VARCHAR(100) NULL
other_postal_code VARCHAR(20) NULL
other_country VARCHAR(100) NULL
skype_id VARCHAR(50) NULL
linkedin VARCHAR(500) NULL
twitter VARCHAR(50) NULL
description TEXT NULL
deleted_at TIMESTAMPTZ NULL (soft-delete)
created_at TIMESTAMPTZ NOT NULL
updated_at TIMESTAMPTZ NOT NULL
created_by UUID FK→users.id
updated_by UUID FK→users.id

Index: (tenant_id, deleted_at), (tenant_id, last_name), (tenant_id, first_name), (tenant_id, email), GIN(tenant_id, to_tsvector('simple', last_name||' '||first_name||' '||email))

company_contacts

Column Type Constraints
company_id UUID FK→companies.id
contact_id UUID FK→contacts.id
tenant_id UUID FK→tenants.id (denormalized for filter)
created_at TIMESTAMPTZ NOT NULL

PK: (company_id, contact_id) Index: (tenant_id), (contact_id)

audit_log

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
user_id UUID FK→users.id
action VARCHAR(50) NOT NULL (create/update/delete/gdpr_delete/login)
entity_type VARCHAR(50) NOT NULL
entity_id UUID NULL
changes JSONB NULL (field→old/new)
timestamp TIMESTAMPTZ NOT NULL

Index: (tenant_id, timestamp), (tenant_id, entity_type), (tenant_id, user_id)

deletion_log (unveränderlich)

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
user_id UUID FK→users.id
entity_type VARCHAR(50) NOT NULL
entity_id UUID NOT NULL
entity_snapshot JSONB NOT NULL (full record before deletion)
deleted_at TIMESTAMPTZ NOT NULL

notifications

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
user_id UUID FK→users.id
type VARCHAR(20) NOT NULL (info/warning/error/success)
title VARCHAR(200) NOT NULL
body TEXT NULL
read_at TIMESTAMPTZ NULL
created_at TIMESTAMPTZ NOT NULL

Index: (tenant_id, user_id, read_at)

password_reset_tokens

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
user_id UUID FK→users.id
token_hash VARCHAR(255) NOT NULL
expires_at TIMESTAMPTZ NOT NULL
used_at TIMESTAMPTZ NULL

plugins (Plugin Registry in DB)

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
name VARCHAR(100) NOT NULL
version VARCHAR(20) NOT NULL
status VARCHAR(20) NOT NULL (installed/active/inactive/error)
manifest JSONB NOT NULL
installed_at TIMESTAMPTZ NOT NULL
activated_at TIMESTAMPTZ NULL

api_tokens (API Token Auth — post-MVP, architecture ready)

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
user_id UUID FK→users.id
token_hash VARCHAR(255) NOT NULL (SHA-256 hash of token)
name VARCHAR(200) NOT NULL (user-provided label)
scopes JSONB NOT NULL (array of scope strings, e.g. ["companies:read","contacts:write"])
expires_at TIMESTAMPTZ NULL (NULL = no expiry)
last_used_at TIMESTAMPTZ NULL
created_at TIMESTAMPTZ NOT NULL
revoked_at TIMESTAMPTZ NULL

Index: (tenant_id, user_id), (token_hash)

Workflow Engine Tables

workflows (Workflow Definition)

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
name VARCHAR(200) NOT NULL
description TEXT NULL
entity_type VARCHAR(50) NOT NULL (company/contact/entry/mail/generic)
trigger_type VARCHAR(30) NOT NULL (manual/event/schedule)
trigger_config JSONB NULL (event name or cron expression)
steps JSONB NOT NULL (ordered array of step definitions)
is_active BOOLEAN default true
created_by UUID FK→users.id
created_at TIMESTAMPTZ NOT NULL
updated_at TIMESTAMPTZ NOT NULL

Index: (tenant_id, entity_type), (tenant_id, is_active)

workflow_instances (Running Workflow)

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
workflow_id UUID FK→workflows.id
entity_type VARCHAR(50) NOT NULL
entity_id UUID NULL
current_step_index INTEGER NOT NULL default 0
status VARCHAR(20) NOT NULL (pending/in_progress/approved/rejected/cancelled/completed)
context JSONB NOT NULL (trigger data + step results)
initiated_by UUID FK→users.id
created_at TIMESTAMPTZ NOT NULL
updated_at TIMESTAMPTZ NOT NULL
completed_at TIMESTAMPTZ NULL

Index: (tenant_id, status), (tenant_id, workflow_id), (tenant_id, initiated_by)

workflow_step_history (Audit Trail per Step)

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
instance_id UUID FK→workflow_instances.id
step_index INTEGER NOT NULL
step_name VARCHAR(200) NOT NULL
action VARCHAR(50) NOT NULL (advance/approve/reject/cancel/timeout)
actor_id UUID FK→users.id NULL (NULL for system)
result JSONB NULL (step output data)
created_at TIMESTAMPTZ NOT NULL

Index: (tenant_id, instance_id, step_index)

KI-Copilot Tables

ai_conversations

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
user_id UUID FK→users.id
role VARCHAR(20) NOT NULL (user/assistant)
content TEXT NOT NULL
proposed_actions JSONB NULL (array of {method, path, body, description})
executed BOOLEAN default false
created_at TIMESTAMPTZ NOT NULL

Index: (tenant_id, user_id, created_at)

DMS Plugin Tables (v2 — Plugin Phase)

dms_folders

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
name VARCHAR(255) NOT NULL
parent_id UUID FK→dms_folders.id NULL
owner_id UUID FK→users.id
path VARCHAR(1000) NOT NULL (materialized path)
deleted_at TIMESTAMPTZ NULL
created_at TIMESTAMPTZ NOT NULL
updated_at TIMESTAMPTZ NOT NULL

Index: (tenant_id, parent_id), (tenant_id, path)

dms_files

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
name VARCHAR(255) NOT NULL
folder_id UUID FK→dms_folders.id NULL
size BIGINT NOT NULL
mime_type VARCHAR(100) NOT NULL
storage_path VARCHAR(1000) NOT NULL
uploaded_by UUID FK→users.id
uploaded_at TIMESTAMPTZ NOT NULL
modified_at TIMESTAMPTZ NOT NULL
deleted_at TIMESTAMPTZ NULL

Index: (tenant_id, folder_id), (tenant_id, deleted_at)

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
file_id UUID FK→dms_files.id
entity_type VARCHAR(50) NOT NULL (company/contact)
entity_id UUID NOT NULL

Unique: (file_id, entity_type, entity_id)

tags

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
name VARCHAR(100) NOT NULL
color VARCHAR(7) default '#808080'
created_at TIMESTAMPTZ NOT NULL

Unique: (tenant_id, name)

tag_assignments

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
entity_type VARCHAR(50) NOT NULL
entity_id UUID NOT NULL
tag_id UUID FK→tags.id

Unique: (entity_type, entity_id, tag_id)

folder_permissions

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
folder_id UUID FK→dms_folders.id
group_id UUID NULL
user_id UUID NULL
permission VARCHAR(10) NOT NULL (read/write/admin)

file_shares

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
file_id UUID FK→dms_files.id
user_id UUID NULL
group_id UUID NULL
permission VARCHAR(10) NOT NULL (read/write)
Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
file_id UUID FK→dms_files.id
token VARCHAR(64) NOT NULL UNIQUE
password_hash VARCHAR(255) NULL
expires_at TIMESTAMPTZ NULL
download_only BOOLEAN default false

Calendar Plugin Tables (v2 — Plugin Phase)

calendars

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
name VARCHAR(200) NOT NULL
color VARCHAR(7) default '#3B82F6'
type VARCHAR(20) NOT NULL (personal/team/project/company)
owner_id UUID FK→users.id
created_at TIMESTAMPTZ NOT NULL

calendar_entries

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
calendar_id UUID FK→calendars.id
entry_type VARCHAR(15) NOT NULL (appointment/task)
subtype VARCHAR(20) default 'normal' (normal/follow_up/private)
title VARCHAR(500) NOT NULL
description TEXT NULL
start_at TIMESTAMPTZ NULL (appointment)
end_at TIMESTAMPTZ NULL (appointment)
all_day BOOLEAN default false
location VARCHAR(500) NULL
due_date TIMESTAMPTZ NULL (task)
priority VARCHAR(10) NULL (high/medium/low) (task)
status VARCHAR(20) default 'open' (open/in_progress/done/cancelled)
assigned_to UUID FK→users.id NULL (task)
reminder JSONB NULL ({value, unit, channel})
recurrence JSONB NULL ({pattern, custom_rule, end_date, exceptions})
source_mail_id UUID NULL (mail integration)
created_at TIMESTAMPTZ NOT NULL
updated_at TIMESTAMPTZ NOT NULL
deleted_at TIMESTAMPTZ NULL

Index: (tenant_id, calendar_id), (tenant_id, start_at), (tenant_id, due_date), (tenant_id, assigned_to, status)

calendar_entry_links

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
entry_id UUID FK→calendar_entries.id
entity_type VARCHAR(50) NOT NULL (company/contact)
entity_id UUID NOT NULL

calendar_shares

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
calendar_id UUID FK→calendars.id
user_id UUID NULL
group_id UUID NULL
permission VARCHAR(10) NOT NULL (read/write)

user_calendar_visibility

Column Type Constraints
user_id UUID FK→users.id
calendar_id UUID FK→calendars.id
tenant_id UUID FK→tenants.id
visible BOOLEAN default true

PK: (user_id, calendar_id)

subtasks

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
entry_id UUID FK→calendar_entries.id
title VARCHAR(500) NOT NULL
completed BOOLEAN default false
created_at TIMESTAMPTZ NOT NULL

resources

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
name VARCHAR(200) NOT NULL
type VARCHAR(50) NOT NULL (room/equipment)

resource_bookings

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
resource_id UUID FK→resources.id
entry_id UUID FK→calendar_entries.id
start_at TIMESTAMPTZ NOT NULL
end_at TIMESTAMPTZ NOT NULL

Mail Plugin Tables (v2 — Plugin Phase)

mail_accounts

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
user_id UUID FK→users.id
name VARCHAR(200) NOT NULL
type VARCHAR(20) default 'personal' (personal/shared)
imap_host VARCHAR(255) NOT NULL
imap_port INTEGER NOT NULL
imap_ssl BOOLEAN default true
smtp_host VARCHAR(255) NOT NULL
smtp_port INTEGER NOT NULL
smtp_starttls BOOLEAN default true
username VARCHAR(255) NOT NULL
password_encrypted BYTEA NOT NULL (AES-256)
default_signature_id UUID NULL

mail_folders

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
account_id UUID FK→mail_accounts.id
name VARCHAR(200) NOT NULL
type VARCHAR(20) NOT NULL (inbox/sent/drafts/spam/custom)
parent_id UUID FK→mail_folders.id NULL
unread_count INTEGER default 0
total_count INTEGER default 0

mails

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
account_id UUID FK→mail_accounts.id
folder_id UUID FK→mail_folders.id
subject TEXT NULL
body_html TEXT NULL (sanitized with DOMPurify)
body_text TEXT NULL
from_addr VARCHAR(255) NOT NULL
to_addrs JSONB NOT NULL (array)
cc_addrs JSONB NULL
bcc_addrs JSONB NULL
date TIMESTAMPTZ NOT NULL
in_reply_to VARCHAR(255) NULL
references TEXT NULL
thread_id VARCHAR(255) NULL
seen BOOLEAN default false
flagged BOOLEAN default false
has_attachments BOOLEAN default false
body_tsv TSVECTOR NULL (full-text search)

Index: GIN(body_tsv), (tenant_id, account_id, folder_id), (tenant_id, thread_id)

mail_attachments

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
mail_id UUID FK→mails.id
filename VARCHAR(255) NOT NULL
mime_type VARCHAR(100) NOT NULL
size BIGINT NOT NULL
dms_file_id UUID FK→dms_files.id NULL
content_id VARCHAR(255) NULL (inline images)

mail_labels

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
name VARCHAR(100) NOT NULL
color VARCHAR(7) NOT NULL

mail_label_assignments

Column Type Constraints
mail_id UUID FK→mails.id
label_id UUID FK→mail_labels.id
tenant_id UUID FK→tenants.id

PK: (mail_id, label_id)

mail_rules

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
account_id UUID FK→mail_accounts.id
name VARCHAR(200) NOT NULL
conditions JSONB NOT NULL
actions JSONB NOT NULL
priority INTEGER default 0

mail_templates

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
user_id UUID FK→users.id NULL (NULL=shared)
name VARCHAR(200) NOT NULL
subject VARCHAR(500) NULL
body_html TEXT NOT NULL

mail_signatures

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
user_id UUID FK→users.id
name VARCHAR(200) NOT NULL
body_html TEXT NOT NULL

vacation_sent_log

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
account_id UUID FK→mail_accounts.id
sender_address VARCHAR(255) NOT NULL
sent_at TIMESTAMPTZ NOT NULL

mail_seen_by

Column Type Constraints
mail_id UUID FK→mails.id
user_id UUID FK→users.id
seen_at TIMESTAMPTZ NOT NULL

PK: (mail_id, user_id)

mail_account_delegates

Column Type Constraints
id UUID PK
account_id UUID FK→mail_accounts.id
delegate_user_id UUID FK→users.id
permission VARCHAR(10) NOT NULL (read/full)

mail_account_send_permissions

Column Type Constraints
account_id UUID FK→mail_accounts.id
user_id UUID FK→users.id

PK: (account_id, user_id)

pgp_keys

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
user_id UUID FK→users.id
private_key_encrypted BYTEA NULL
public_key TEXT NOT NULL

contact_pgp_keys

Column Type Constraints
contact_id UUID FK→contacts.id
public_key TEXT NOT NULL
tenant_id UUID FK→tenants.id

PK: (contact_id)

PostgreSQL tsvector with GIN index on:

  • contacts: to_tsvector('simple', last_name || ' ' || first_name || ' ' || COALESCE(email, ''))
  • companies: to_tsvector('simple', name || ' ' || COALESCE(description, '') || ' ' || COALESCE(billing_city, ''))
  • mails: to_tsvector('simple', subject || ' ' || body_text) stored in body_tsv column

3. API Design

Conventions

  • Versioning: URL-based: /api/v1/...
  • Auth: Session cookie (HttpOnly, Secure, SameSite=Strict). All endpoints except /api/v1/health and /api/v1/auth/* require auth.
  • Tenant Context: Active tenant stored in session. All API responses are tenant-scoped.
  • Error Format: {"detail": "message", "code": "error_code", "fields": {"field": "msg"}}
  • Pagination: ?page=1&page_size=25{items: [...], total: N, page: P, page_size: S}
  • Sort: ?sort_by=name&sort_order=asc|desc
  • Search: ?search=text → full-text search
  • Filter: ?industry=IT&country=Germany → structured filters
  • Export: ?format=csv|xlsx on list endpoints
  • CSRF: SameSite=Strict + Origin-Header-Validierung (no double-submit token)

Auth Endpoints

Method Path Feature Description
POST /api/v1/auth/login F-AUTH-01 Login with email+password, sets session cookie
POST /api/v1/auth/logout F-AUTH-02 Invalidate session, clear cookie
GET /api/v1/auth/me F-AUTH-01 Get current user + active tenant
POST /api/v1/auth/switch-tenant F-AUTH-07 Switch active tenant
POST /api/v1/auth/password-reset/request F-AUTH-05 Request password reset email
POST /api/v1/auth/password-reset/confirm F-AUTH-05 Reset password with token

User Management Endpoints

Method Path Feature Description
GET /api/v1/users F-AUTH-03 List users (admin only, paginated)
POST /api/v1/users F-AUTH-03 Create user (admin only)
GET /api/v1/users/{id} F-AUTH-03 Get user details
PATCH /api/v1/users/{id} F-AUTH-03 Update user
DELETE /api/v1/users/{id} F-AUTH-03 Delete user
GET /api/v1/users/me/settings F-CORE-09 Get current user preferences
PATCH /api/v1/users/me/settings F-CORE-09 Update preferences (language, theme, etc.)

Role Management Endpoints

Method Path Feature Description
GET /api/v1/roles F-AUTH-06 List roles
POST /api/v1/roles F-AUTH-06 Create role
PATCH /api/v1/roles/{id} F-AUTH-06 Update role (incl. field permissions)
DELETE /api/v1/roles/{id} F-AUTH-06 Delete role

Tenant Management Endpoints

Method Path Feature Description
GET /api/v1/tenants F-AUTH-07 List tenants for current user
POST /api/v1/tenants F-AUTH-07 Create tenant (admin)
GET /api/v1/tenants/{id}/users F-AUTH-07 List users in tenant
POST /api/v1/tenants/{id}/users F-AUTH-07 Assign user to tenant

Company Endpoints (F-DATA-01, F-DATA-02)

Method Path Feature Description
GET /api/v1/companies F-COMP-05,06 List with search, filter, pagination, sort
POST /api/v1/companies F-COMP-01 Create company
GET /api/v1/companies/{id} F-COMP-02 Get company detail (incl. contacts)
PUT /api/v1/companies/{id} F-COMP-03 Update company
DELETE /api/v1/companies/{id} F-COMP-04 Soft-delete (?cascade=true)
POST /api/v1/companies/{id}/contacts/{contact_id} F-CONT-07 Add N:M contact link
DELETE /api/v1/companies/{id}/contacts/{contact_id} F-CONT-07 Remove N:M contact link
GET /api/v1/companies/export F-DATA-01,02 Export (?format=csv
GET /api/v1/companies/{id}/emails F-MAIL-10 Company email history

Contact Endpoints

Method Path Feature Description
GET /api/v1/contacts F-CONT-05,06 List with search, filter, pagination
POST /api/v1/contacts F-CONT-01 Create contact (with company_ids array)
GET /api/v1/contacts/{id} F-CONT-02 Get contact detail (incl. companies)
PUT /api/v1/contacts/{id} F-CONT-03 Update contact
DELETE /api/v1/contacts/{id} F-CONT-04 Soft-delete
DELETE /api/v1/contacts/{id}?gdpr=true F-COMP-08 Hard delete (DSGVO)
GET /api/v1/contacts/export F-DATA-01,02 Export (?format=csv
GET /api/v1/contacts/{id}/emails F-MAIL-10 Contact email history

Import Endpoints

Method Path Feature Description
POST /api/v1/import F-MIG-01 CSV import (entity_type + file)
POST /api/v1/import/preview F-CORE-11 Dry-run import preview

Audit Log Endpoints

Method Path Feature Description
GET /api/v1/audit-log F-COMP-07 List audit log (admin, paginated)

Notification Endpoints

Method Path Feature Description
GET /api/v1/notifications F-CORE-13 List notifications (unread first)
PATCH /api/v1/notifications/{id}/read F-CORE-13 Mark as read
GET /api/v1/notifications/unread-count F-CORE-13 Unread badge count
PATCH /api/v1/users/me/notification-prefs F-CORE-13 Update notification preferences

Global Search Endpoint (F-COMP-06, F-CONT-06, F-SEARCH-01)

Method Path Feature Description
GET /api/v1/search?q=text F-SEARCH-01 Search across all entities

Health & Infrastructure

Method Path Feature Description
GET /api/v1/health F-INFRA-01 Health check (no auth)
GET /api/v1/jobs/{id}/status F-SCHED-01 Background job status

Plugin System Endpoints

Method Path Feature Description
GET /api/v1/plugins F-PLUGIN-01 List plugins (status)
POST /api/v1/plugins/{name}/install F-PLUGIN-02 Install plugin
POST /api/v1/plugins/{name}/activate F-PLUGIN-02 Activate plugin
POST /api/v1/plugins/{name}/deactivate F-PLUGIN-02 Deactivate plugin
DELETE /api/v1/plugins/{name} F-PLUGIN-02 Uninstall (?remove_data=true)
GET /api/v1/plugins/manifest F-PLUGIN-02 Plugin manifest schema (for developers)

DMS Plugin Endpoints (v2 — Plugin Phase)

v2 Plugin Features — These endpoints are implemented in T04 (DMS Backend) and T08a (DMS Frontend). Not available in v1.

Method Path Feature Description
GET /api/v1/dms/folders F-FILEUI-01 List folders (tree)
POST /api/v1/dms/folders F-DMS-01 Create folder
PATCH /api/v1/dms/folders/{id} F-DMS-01 Rename/move folder
DELETE /api/v1/dms/folders/{id} F-DMS-01 Soft-delete folder
POST /api/v1/dms/files/upload F-DMS-02 Upload file (multipart)
GET /api/v1/dms/files/{id} F-DMS-06 File metadata
PATCH /api/v1/dms/files/{id} F-DMS-03 Rename/move file
DELETE /api/v1/dms/files/{id} F-DMS-03 Soft-delete file
POST /api/v1/dms/files/{id}/restore F-DMS-03 Restore from trash
GET /api/v1/dms/files/{id}/preview F-DMS-04 PDF preview stream
POST /api/v1/dms/files/{id}/edit-session F-DMS-05 OnlyOffice edit session
GET /api/v1/dms/files/{id}/permissions F-PERM-06 Permission overview
POST /api/v1/dms/files/{id}/link F-LINK-01,02 Link to entity
DELETE /api/v1/dms/files/{id}/link F-LINK-05 Remove entity link
POST /api/v1/dms/files/{id}/share F-PERM-03,04 Share with user/group
DELETE /api/v1/dms/files/{id}/share F-PERM-03,04 Remove share
POST /api/v1/dms/files/{id}/share-link F-PERM-05 Create public share link
GET /api/v1/dms/search F-DMS-07 Search files (?folder_id)
GET /api/v1/dms/shared-with-me F-PERM-03 Shared files list
POST /api/v1/dms/files/bulk-move F-FILEUI-04 Bulk move
POST /api/v1/dms/files/bulk-delete F-FILEUI-04 Bulk delete

Tag Plugin Endpoints

Method Path Feature Description
GET /api/v1/tags F-TAG-04 List tags (with counts)
POST /api/v1/tags F-TAG-02 Create tag (admin)
PATCH /api/v1/tags/{id} F-TAG-02 Update tag
DELETE /api/v1/tags/{id} F-TAG-02 Delete tag (cascade)
POST /api/v1/tags/assign F-TAG-01 Assign tag to entity
DELETE /api/v1/tags/assign F-TAG-01 Remove tag from entity
POST /api/v1/tags/bulk-assign F-FILEUI-04 Bulk tag assign

Calendar Plugin Endpoints

Method Path Feature Description
GET /api/v1/calendars F-CAL-11 List calendars
POST /api/v1/calendars F-CAL-11 Create calendar
PATCH /api/v1/calendars/{id} F-CAL-11 Update calendar
DELETE /api/v1/calendars/{id} F-CAL-11 Delete calendar (cascade)
POST /api/v1/calendars/{id}/share F-CAL-13 Share calendar
GET /api/v1/calendars/{id}/permissions F-CAL-13 Calendar permissions
GET /api/v1/calendar/entries F-CAL-01,17 List entries (filter, paginate)
POST /api/v1/calendar/entries F-CAL-03 Create entry (appointment/task)
GET /api/v1/calendar/entries/{id} F-CAL-03 Get entry detail
PATCH /api/v1/calendar/entries/{id} F-CAL-05 Update entry (drag&drop, status)
DELETE /api/v1/calendar/entries/{id} Delete entry
POST /api/v1/calendar/entries/{id}/link F-CAL-04 Link to entity
POST /api/v1/calendar/entries/{id}/subtasks F-CAL-16 Create subtask
PATCH /api/v1/calendar/entries/{id}/subtasks/{sub_id} F-CAL-16 Toggle subtask
POST /api/v1/calendar/entries/bulk F-CAL-18 Bulk actions
GET /api/v1/calendar/kanban F-CAL-02 Kanban board view
GET /api/v1/calendar/entries/export F-CAL-17 Export tasks CSV
GET /api/v1/calendar/{calendar_id}/ics-feed F-CAL-09 ICS feed export
POST /api/v1/calendar/import F-CAL-09 ICS import
POST /api/v1/resources F-CAL-10 Create resource (admin)
POST /api/v1/calendar/entries/{id}/book-resource F-CAL-10 Book resource

Mail Plugin Endpoints

Method Path Feature Description
GET /api/v1/mail/accounts F-MAIL-14 List mail accounts
POST /api/v1/mail/accounts F-MAIL-18 Create mail account
PATCH /api/v1/mail/accounts/{id} F-MAIL-14 Update account
GET /api/v1/mail/accounts/shared F-MAIL-15 Shared mailboxes
POST /api/v1/mail/accounts/{id}/users F-MAIL-15 Assign shared mailbox users
POST /api/v1/mail/accounts/{id}/delegates F-MAIL-16 Delegate access
POST /api/v1/mail/accounts/{id}/send-permissions F-MAIL-17 Grant send permission
GET /api/v1/mail/folders F-MAIL-01,19 List mail folders
POST /api/v1/mail/folders F-MAIL-19 Create folder
PATCH /api/v1/mail/folders/{id} F-MAIL-19 Rename folder
DELETE /api/v1/mail/folders/{id} F-MAIL-19 Delete folder
GET /api/v1/mail F-MAIL-03 List mails (filter, search, paginate)
GET /api/v1/mail/{id} F-MAIL-02 Get mail detail
POST /api/v1/mail/send F-MAIL-02 Send mail
POST /api/v1/mail/{id}/reply F-MAIL-02 Reply to mail
POST /api/v1/mail/{id}/forward F-MAIL-02 Forward mail
PATCH /api/v1/mail/{id}/flags F-MAIL-09 Update flags (seen/flagged)
GET /api/v1/mail/{id}/attachments/{att_id} F-MAIL-04 Download attachment
POST /api/mail/{id}/link F-MAIL-10 Manual contact/company link
POST /api/v1/mail/{id}/create-event F-MAIL-11 Create calendar event from mail
GET /api/v1/mail/search F-MAIL-03 Full-text mail search
GET /api/v1/mail/threads F-MAIL-05 Threaded view
POST /api/v1/mail/templates F-MAIL-06 Create template
GET /api/v1/mail/templates F-MAIL-06 List templates
POST /api/v1/mail/signatures F-MAIL-13 Create signature
GET /api/v1/mail/signatures F-MAIL-13 List signatures
POST /api/v1/mail/rules F-MAIL-07 Create mail rule
GET /api/v1/mail/rules F-MAIL-07 List rules
DELETE /api/v1/mail/rules/{id} F-MAIL-07 Delete rule
POST /api/v1/mail/vacation F-MAIL-08 Set vacation auto-reply
POST /api/v1/mail/pgp/keys F-MAIL-12 Import PGP private key
POST /api/v1/contacts/{id}/pgp-key F-MAIL-12 Import contact public key
POST /api/v1/mail/labels F-MAIL-09 Create label
POST /api/v1/mail/{id}/labels F-MAIL-09 Assign label to mail

Public Endpoints (no auth)

Method Path Feature Description
GET /api/v1/health F-INFRA-01 Health check
GET /api/public/share/{token} F-PERM-05 Public share link access
GET /api/v1/calendar/{calendar_id}/ics-feed?token=... F-CAL-09 ICS feed (token auth)

KI-Copilot Endpoints

Method Path Feature Description
POST /api/v1/ai/copilot/query F-AI-01 Natural language → proposed API calls
GET /api/v1/ai/copilot/history F-AI-01 Conversation history (per user, paginated)
POST /api/v1/ai/copilot/execute F-AI-01 Execute proposed API action (with user confirmation)

Workflow Engine Endpoints

Method Path Feature Description
GET /api/v1/workflows F-WF-01 List workflow definitions (paginated, filter by entity_type)
POST /api/v1/workflows F-WF-01 Create workflow definition (admin only)
GET /api/v1/workflows/{id} F-WF-01 Get workflow definition detail
PATCH /api/v1/workflows/{id} F-WF-01 Update workflow definition
DELETE /api/v1/workflows/{id} F-WF-01 Delete workflow definition
POST /api/v1/workflows/{id}/instances F-WF-01 Start workflow instance (trigger)
GET /api/v1/workflows/instances F-WF-01 List workflow instances (filter by status, paginated)
GET /api/v1/workflows/instances/{id} F-WF-01 Get instance detail (current step, history)
POST /api/v1/workflows/instances/{id}/advance F-WF-01 Advance to next step (approve/reject)
POST /api/v1/workflows/instances/{id}/cancel F-WF-01 Cancel workflow instance

Monitoring Endpoints

Method Path Feature Description
GET /api/v1/health F-INFRA-01,F-INFRA-04 Extended health check (DB, Redis, storage, worker)
GET /api/v1/metrics F-INFRA-04 Prometheus metrics (admin only)

4. Plugin Architecture

Plugin Manifest Format

{
  "name": "dms",
  "version": "1.0.0",
  "display_name": "Dateien (DMS)",
  "description": "Datei-Management-System mit Ordnerstruktur, Upload, OnlyOffice",
  "author": "LeoCRM Team",
  "dependencies": [],
  "min_core_version": "1.0.0",
  "services": ["db", "cache", "event_bus", "storage", "notifications"],
  "endpoints": {
    "router": "app.plugins.builtins.dms.router:router"
  },
  "migrations": {
    "package": "app.plugins.builtins.dms.migrations",
    "versions_dir": "migrations"
  },
  "ui": {
    "routes": [{"path": "/dms", "component": "DmsView"}],
    "menu_items": [{"label": "Dateien", "icon": "folder", "route": "/dms", "order": 40}],
    "detail_tabs": [
      {"entity": "company", "tab_id": "files", "label": "Dateien", "component": "CompanyFilesTab"},
      {"entity": "contact", "tab_id": "files", "label": "Dateien", "component": "ContactFilesTab"}
    ],
    "settings_pages": [{"id": "dms-settings", "label": "DMS-Einstellungen", "component": "DmsSettings"}],
    "dashboard_widgets": []
  },
  "events": {
    "listens_to": ["company.deleted", "contact.deleted"],
    "emits": ["dms.file.uploaded", "dms.file.shared"]
  },
  "preferences": [
    {"key": "default_folder_view", "type": "string", "default": "grid", "options": ["grid", "list"]}
  ],
  "notification_types": ["dms.share_received", "dms.upload_complete"]
}

Lifecycle Hooks

install()   → Run DB migrations, register plugin in DB (status=installed)
activate()  → Register routes, event listeners, UI components (status=active)
deactivate()→ Unregister routes, event listeners, UI components (status=inactive)
uninstall() → Optional: drop plugin tables (?remove_data=true, status=removed)

Event Bus (F-CORE-01)

v1 Core Feature — Event Bus is part of T01 (Core Infrastructure).

# Publishing events
await event_bus.publish("company.created", payload={"company_id": uuid, "tenant_id": uuid})
await event_bus.publish("contact.deleted", payload={"contact_id": uuid, "tenant_id": uuid})

# Subscribing (plugins register during activate())
@event_bus.subscribe("company.created")
async def on_company_created(event):
    # e.g., create default folder for company
    ...
``

**Implementation:** Async in-process event bus. Events are dispatched to registered handlers. For persistence/retry, events are also pushed to Redis job queue for reliable processing.

### Service Container / DI (F-CORE-05, F-CORE-07, F-CORE-08, F-CORE-10, F-CORE-12)

> **v1 Core Features:**
> - F-CORE-07: Async Job Queue (ARQ)  background jobs for export, backup, mail sync
> - F-CORE-08: Caching-Strategie  Redis-based caching for sessions, query results, plugin metadata
> - F-CORE-10: Storage-Backend  abstract storage interface (local filesystem, S3-compatible)
> - F-CORE-12: PDF/Document Generation Service  WeasyPrint for PDF generation (export, reports)

```python
# Core services registered in container
class ServiceContainer:
    db: SessionManager
    cache: RedisCache
    event_bus: EventBus
    storage: StorageBackend
    notifications: NotificationService
    audit: AuditLogger
    jobs: JobQueue

# Plugin requests services via manifest
def activate(container: ServiceContainer):
    db = container.db
    event_bus = container.event_bus
    ...

Plugin DB Migration (F-CORE-03)

Each plugin has its own migrations/ directory with versioned SQL/Python files. Migrations are tracked in a plugin_migrations table:

Column Type
plugin_name VARCHAR
migration_id VARCHAR
executed_at TIMESTAMPTZ

All plugin tables include tenant_id column for isolation.

UI Plugin Framework (F-CORE-04)

The backend plugin manifest declares UI components. The frontend PluginRegistry.tsx fetches active plugin manifests on startup and dynamically renders:

  • Routes (React Router)
  • Menu items (Sidebar)
  • Detail tabs (Company/Contact detail views)
  • Settings pages (Settings tree)
  • Dashboard widgets

Plugin UI components are lazy-loaded via React.lazy() with Suspense boundaries.


5. Multi-Tenant Architecture

Implementation (F-CORE-02)

  1. Session Context: Active tenant_id stored in session. Set at login (default tenant) or via switch-tenant.

  2. ORM Auto-Filtering: SQLAlchemy before_query event listener automatically injects tenant_id filter on all models that have a tenant_id column. This prevents cross-tenant data access at the ORM level.

@event.listens_for(Session, "do_orm_execute")
def auto_filter_tenant(execute_state):
    if not _tenant_filter_disabled:
        execute_state.statement = execute_state.statement.where(
            TenantMixin.tenant_id == current_tenant_id()
        )
  1. TenantMixin: All models with tenant isolation inherit from a TenantMixin base class that adds tenant_id column.

  2. Tenant Switch: POST /api/v1/auth/switch-tenant updates session. All subsequent queries use new tenant_id.

  3. Cross-Tenant Protection: If a user tries to access a resource not in their active tenant → 404 (not 403, to prevent information leakage).

  4. Plugin Tables: All plugin-created tables MUST include tenant_id. The plugin migration validator checks this.


6. Auth Architecture

Session-Based Auth (F-AUTH-01, F-INT-02)

Login Flow:
1. POST /api/v1/auth/login {email, password}
2. Backend validates → bcrypt.check_password(password, user.password_hash)
3. Create session in Redis (key: `session:{session_id}`, value: {user_id, tenant_id, csrf_token}, TTL=8h)
4. Write session record to PostgreSQL `sessions` table for audit trail (id, user_id, tenant_id, csrf_token, expires_at, created_at)
5. Set cookie: leocrm_session=<session_id>; HttpOnly; Secure; SameSite=Strict; Path=/
6. Return {user, tenant} info

RBAC (F-AUTH-04, F-AUTH-06, F-AUTH-08)

Role → Permissions Structure:
{
  "companies": {"read": true, "write": true, "delete": true, "admin": false},
  "contacts": {"read": true, "write": true, "delete": true, "admin": false},
  "users": {"read": false, "write": false, "delete": false, "admin": false},
  ...
}

Field-Level Permissions:
{
  "companies.annual_revenue": "hidden",  // viewer can't see
  "companies.phone": "read_only",       // editor can't edit
  "contacts.email": "read_write"         // full access
}

Default roles: admin (all permissions), editor (CRUD on companies/contacts, no user management), viewer (read-only).

Custom roles can be created via POST /api/v1/roles with custom permissions.

API Tokens (F-INT-02)

API tokens for external integrations (post-MVP, but architecture supports it):

  • Tokens stored in api_tokens table (user_id, token_hash, name, scopes, expires_at)
  • Token auth via Authorization: Bearer <token> header
  • Token respects RBAC and tenant isolation

CSRF Protection (F-SEC-01, F-SEC-03)

v1 Core Feature — CSRF protection via SameSite=Strict + Origin validation.

  • SameSite=Strict cookie (browser blocks cross-site requests)
  • Origin-Header-Validierung middleware (server-side check)
  • Only GET/HEAD/OPTIONS are exempt

Content-Security-Policy (F-SEC-02)

  • CSP-Header gesetzt im Nginx-Reverse-Proxy für alle Responses: Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-src 'self' https://onlyoffice:8080; object-src 'none'; base-uri 'self'; form-action 'self'
  • X-Content-Type-Options: nosniff
  • X-Frame-Options: SAMEORIGIN (nur OnlyOffice iframe erlaubt)
  • X-XSS-Protection: 1; mode=block
  • Strict-Transport-Security: max-age=31536000; includeSubDomains
  • Referrer-Policy: strict-origin-when-cross-origin
  • HTML in User-Eingaben wird serverseitig via Pydantic validiert und im Frontend via DOMPurify/escaped rendering sanitized

CORS Configuration

v1 Core Feature — Cross-Origin Resource Sharing policy for API security.

  • Production: Same-origin only (Access-Control-Allow-Origin: <self>). Frontend and API served from same domain via Nginx reverse proxy.
  • Development: Explicit origins allowed: http://localhost:5173 (Vite dev), http://localhost:3000 (alt dev). No wildcard *.
  • Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS
  • Headers: Authorization, Content-Type, X-CSRF-Token, X-Tenant-ID
  • Credentials: true (cookies allowed for session auth)
  • Max-Age: 3600 (preflight cache)
  • Implementation: FastAPI CORSMiddleware with explicit origin list, not wildcard.

Auth Rate Limiting (Brute-Force Protection)

v1 Core Feature — Rate limiting for authentication endpoints to prevent brute-force attacks.

  • Login endpoint (POST /api/v1/auth/login):
    • Redis-based counter: auth:login:{ip}:{email} with TTL=15min
    • Max 5 failed attempts per 15min window → 429 Too Many Requests
    • On success: counter reset
    • On failure: counter incremented, response includes Retry-After header
  • Password reset request (POST /api/v1/auth/password-reset/request):
    • Redis-based counter: auth:reset:{ip} with TTL=1h
    • Max 3 requests per hour → 429 (prevents email enumeration via timing)
    • Always returns 200 (no user enumeration) but blocks after limit
  • Password reset confirm (POST /api/v1/auth/password-reset/confirm):
    • Redis-based counter: auth:reset_confirm:{ip} with TTL=1h
    • Max 5 attempts per hour → 429
  • General API rate limiting (all endpoints):
    • Redis-based sliding window: rate:{ip}:{endpoint} with TTL=1min
    • Default: 60 requests/min per IP per endpoint
    • Auth endpoints: 10 requests/min per IP (stricter)
  • Implementation: FastAPI middleware using Redis INCR + EXPIRE

PostgreSQL Row-Level Security (RLS) — Defense-in-Depth

v1 Core Feature — Database-level tenant isolation as defense-in-depth alongside ORM-level tenant filtering.

Strategy: ORM-level tenant filtering (SQLAlchemy event listener) is the primary isolation mechanism. PostgreSQL RLS policies provide a secondary defense layer to prevent data leaks if ORM filtering is bypassed (raw SQL, ARQ worker jobs, admin queries).

Implementation:

-- Enable RLS on all tenant-scoped tables
ALTER TABLE companies ENABLE ROW LEVEL SECURITY;
ALTER TABLE contacts ENABLE ROW LEVEL SECURITY;
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE roles ENABLE ROW LEVEL SECURITY;
ALTER TABLE sessions ENABLE ROW LEVEL SECURITY;
ALTER TABLE audit_log ENABLE ROW LEVEL SECURITY;
ALTER TABLE notifications ENABLE ROW LEVEL SECURITY;
ALTER TABLE api_tokens ENABLE ROW LEVEL SECURITY;
-- (v2: dms_folders, dms_files, calendars, mail_accounts, etc.)

-- Create policy: users can only see rows in their current tenant
CREATE POLICY tenant_isolation ON companies
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- Same pattern for all tenant-scoped tables
CREATE POLICY tenant_isolation ON contacts
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- ... (repeat for all tenant-scoped tables)

-- Application sets tenant context per request:
-- SET LOCAL app.current_tenant_id = '<uuid>';
-- This is set in SQLAlchemy session event listener after tenant resolution

Tenant Context Propagation:

  • API requests: SQLAlchemy before_request event sets app.current_tenant_id session variable
  • ARQ worker jobs: Job payload includes tenant_id; worker sets session variable before processing
  • Raw SQL queries: Must explicitly set tenant context or use SET LOCAL
  • Admin/superuser: Can bypass RLS via SET row_security = off (logged in audit_log)

Testing:

  • Integration test: create 2 tenants, verify tenant A cannot read tenant B's data even with raw SQL
  • Test: ARQ worker processes job with correct tenant context
  • Test: RLS bypass attempt is logged in audit_log

Password Reset (F-AUTH-05, F-INT-01)

v1 Core Feature — F-INT-01: E-Mail-Integration for password reset via SMTP.

1. POST /api/v1/auth/password-reset/request {email}
   → Generate token, store hash in `password_reset_tokens` (24h expiry)
   → Send email with reset link
   → Always return 200 (no user enumeration)

2. POST /api/v1/auth/password-reset/confirm {token, new_password}
   → Verify token hash + expiry
   → Update password_hash
   → Mark token as used
   → Invalidate all sessions for user

7. Frontend Architecture

Stack (F-UI-01, F-UI-02, F-UI-03, F-UI-04, F-UI-05, F-UI-06, F-UI-08)

Component Technology
Framework React 18
Build Tool Vite
Router React Router v6
State (Server) TanStack Query (React Query v5)
State (Client) Zustand
i18n react-i18next
Forms React Hook Form + Zod
Styling Tailwind CSS
Icons lucide-react
Tables TanStack Table
Calendar Custom (FullCalendar or custom)
Rich Text TipTap (mail composer)
PDF Viewer PDF.js
Testing Vitest + @testing-library/react

Routing (F-NAV-01, F-SET-01)

/                 → Dashboard
/login            → Login page
/password-reset   → Password reset flow
/companies        → Company list
/companies/:id    → Company detail
/contacts         → Contact list
/contacts/:id     → Contact detail
/calendar         → Calendar (plugin)
/dms              → DMS file browser (plugin)
/mail             → Mail (plugin)
/users            → User management (admin)
/audit-log        → Audit log (admin)
/settings         → Settings tree
/settings/*       → Settings sub-pages
/search           → Global search results
/*                → Plugin routes (dynamic)

State Management

  • TanStack Query: Server state (API data, caching, optimistic updates, loading/error states).
  • Zustand: Client state (active tenant, sidebar collapsed, theme, language, UI toggles).
  • URL State: Filters, pagination, sort via URL search params (shareable URLs).

i18n

  • Locale files: src/i18n/locales/de.json, src/i18n/locales/en.json
  • Default: German. Fallback: German.
  • Date/time format via date-fns with locale.
  • Language stored in user preferences (synced to backend).

Accessibility (F-A11Y-01, F-A11Y-02, F-A11Y-03)

  • Semantic HTML, ARIA roles on all interactive elements.
  • prefers-reduced-motion media query in global CSS.
  • 44px touch targets via Tailwind min-h-[44px] min-w-[44px] on buttons/links.
  • .sr-only class for screen-reader-only text.
  • Keyboard navigation: logical tab order, focus visible.
  • WCAG 2.1 AA contrast (>4.5:1).

Design System

Based on the approved prototype (leocrm-prototype-x7k2p9):

  • Color tokens via Tailwind config
  • Component library: Button, Input, Select, Modal, Toast, Table, Card, Badge, Avatar, Pagination, EmptyState, Skeleton
  • Responsive breakpoints: sm: 375px, md: 768px, lg: 1024px, xl: 1280px

8. Deployment Architecture

Docker Compose

services:
  backend:
    build: ./backend
    ports: ["8000:8000"]
    env_file: .env
    depends_on: [postgres, redis]
    healthcheck:
      test: curl -f http://localhost:8000/api/v1/health || exit 1
      interval: 30s
      timeout: 10s
      retries: 3

  frontend:
    build: ./frontend
    ports: ["80:80"]
    depends_on: [backend]

  postgres:
    image: postgres:16-alpine
    volumes: ["pgdata:/var/lib/postgresql/data"]
    env_file: .env

  redis:
    image: redis:7-alpine
    volumes: ["redisdata:/data"]

  worker:
    build: ./backend
    command: arq app.core.jobs.WorkerSettings
    env_file: .env
    depends_on: [postgres, redis]

  onlyoffice:
    image: onlyoffice/documentserver:latest
    ports: ["8080:80"]
    volumes: ["onlyoffice_data:/var/www/onlyoffice/Data"]

volumes:
  pgdata:
  redisdata:
  onlyoffice_data:
  storage_data:

Environment Variables (F-ENV-01)

v1 Core Feature — F-ENV-01: Environments & Secrets — dev/test/prod profiles with .env.example.

# Database
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DB=leocrm
POSTGRES_USER=leocrm
POSTGRES_PASSWORD=<secret>

# Redis
REDIS_URL=redis://redis:6379/0

# Session
LEOCRM_SECRET_KEY=<min-32-chars>
SESSION_TIMEOUT_HOURS=8

# SMTP (for password reset + mail plugin)
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASS=

# Mail encryption key
MAIL_ENCRYPTION_KEY=<32-byte-hex>

# Storage
STORAGE_BACKEND=local  # or s3
STORAGE_PATH=/data/leocrm/storage
S3_ENDPOINT=
S3_BUCKET=
S3_ACCESS_KEY=
S3_SECRET_KEY=

# OnlyOffice
ONLYOFFICE_URL=http://onlyoffice:80

# Logging
LOG_LEVEL=INFO

Backup (F-INFRA-02)

v1 Core Feature — F-INFRA-02: Backup & Restore — pg_dump + file storage backup.

  • pg_dump daily cron job → backup volume or S3
  • Storage volume backup (files)
  • Restore documented in docs/admin-guide.md

8b. KI-Integration (F-AI-01)

Architektur-Grundlage

LeoCRM ist API-First (F-CORE-06): alle Features sind über die REST API nutzbar. Der KI-Copilot nutzt dieselbe API wie das Frontend — er ist ein API-Client mit eigener Authentifizierung.

KI-Copilot API-Endpoint

Method Path Feature Description
POST /api/v1/ai/copilot/query F-AI-01 Natural language → API call translation
GET /api/v1/ai/copilot/history F-AI-01 Conversation history (per user)
POST /api/v1/ai/copilot/execute F-AI-01 Execute proposed API action (with user confirmation)

RBAC-Durchsetzung

Der KI-Copilot respektiert das Rollen-/Rechte-System des zugehörigen Users:

  1. Authentifizierung: Copilot-Requests verwenden die Session des Users (gleicher Cookie). Keine separate Copilot-Auth.
  2. RBAC: Copilot-API-Calls gehen durch dieselbe RBAC-Middleware wie Frontend-Calls. Ein Viewer-Copilot kann keine Daten löschen.
  3. Field-Level Permissions: Copilot sieht nur Felder, die die User-Rolle erlaubt (hidden fields werden aus Responses gefiltert).
  4. Tenant-Isolation: Copilot ist an den tenant_id der aktiven Session gebunden. Cross-Tenant → 404.
  5. Audit Log: Alle Copilot-Aktionen werden im Audit-Log als entity_type=ai_copilot protokolliert.

Implementation-Modell (v1)

v1 implementiert die API-Schnittstelle (/api/v1/ai/copilot/*) mit:

  • Query-Endpoint: Nimmt Natural-Language-Input → returns vorgeschlagene API-Calls (Method, Path, Body) + Erklärung
  • Execute-Endpoint: Führt den vorgeschlagenen API-Call aus (mit User-Bestätigung im Frontend)
  • History: Speichert Konversationsverlauf pro User
  • Die eigentliche LLM-Integration (z.B. OpenAI/Anthropic) ist konfigurierbar via Env-Vars (AI_MODEL, AI_API_KEY)

Frontend-Integration

  • Sidebar-Eintrag „KI-Copilot" (sichtbar wenn AI_ENABLED=true)
  • Chat-Interface mit Vorschlags-Karten (zeigt geplante API-Calls vor Ausführung)
  • User muss jede destruktive Aktion bestätigen (Confirm-Dialog)

DB-Tabelle: ai_conversations

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
user_id UUID FK→users.id
role VARCHAR(20) NOT NULL (user/assistant)
content TEXT NOT NULL
proposed_actions JSONB NULL (array of {method, path, body, description})
executed BOOLEAN default false
created_at TIMESTAMPTZ NOT NULL

Index: (tenant_id, user_id, created_at)


8c. Workflow Engine (F-WF-01)

Hybrid-Ansatz

Zwei Engine-Typen:

  1. Code-Engine (Kern-Workflows): Hartkodierte Python-Workflows für System-Prozesse (z.B. User-Onboarding, Plugin-Installation-Sequence, Mail-Sync-Trigger). Definiert als async def Funktionen im app/workflows/ Modul. Nicht vom User konfigurierbar.

  2. Configurable Workflow-Engine (User-Workflows): User-definierte Prozesse via Admin-UI. Mehrstufige Prozesse mit Bedingungen, Genehmigungs-Ketten, Notifications.

Workflow API Endpoints

Method Path Feature Description
GET /api/v1/workflows F-WF-01 List workflow definitions
POST /api/v1/workflows F-WF-01 Create workflow definition (admin)
GET /api/v1/workflows/{id} F-WF-01 Get workflow definition
PATCH /api/v1/workflows/{id} F-WF-01 Update workflow definition
DELETE /api/v1/workflows/{id} F-WF-01 Delete workflow definition
POST /api/v1/workflows/{id}/instances F-WF-01 Start workflow instance (trigger)
GET /api/v1/workflows/instances F-WF-01 List workflow instances (filter by status)
GET /api/v1/workflows/instances/{id} F-WF-01 Get instance detail (current step, history)
POST /api/v1/workflows/instances/{id}/advance F-WF-01 Advance to next step (approve/reject)
POST /api/v1/workflows/instances/{id}/cancel F-WF-01 Cancel workflow instance

DB-Tabellen

workflows (Workflow Definition)

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
name VARCHAR(200) NOT NULL
description TEXT NULL
entity_type VARCHAR(50) NOT NULL (company/contact/entry/mail/generic)
trigger_type VARCHAR(30) NOT NULL (manual/event/schedule)
trigger_config JSONB NULL (event name or cron expression)
steps JSONB NOT NULL (ordered array of step definitions)
is_active BOOLEAN default true
created_by UUID FK→users.id
created_at TIMESTAMPTZ NOT NULL
updated_at TIMESTAMPTZ NOT NULL

Index: (tenant_id, entity_type), (tenant_id, is_active)

steps JSONB Structure:

[
  {
    "id": "step_1",
    "name": "Angebot erstellen",
    "type": "action",
    "action": "create_entity",
    "entity": "company",
    "fields": {"name": "${trigger.company_name}", "industry": "${trigger.industry}"}
  },
  {
    "id": "step_2",
    "name": "Genehmigung durch Manager",
    "type": "approval",
    "approver_role": "admin",
    "timeout_hours": 48,
    "on_reject": "notify_initiator"
  },
  {
    "id": "step_3",
    "name": "Benachrichtigung senden",
    "type": "notification",
    "template": "offer_approved",
    "recipients": ["initiator"]
  }
]

workflow_instances (Running Workflow)

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
workflow_id UUID FK→workflows.id
entity_type VARCHAR(50) NOT NULL
entity_id UUID NULL
current_step_index INTEGER NOT NULL default 0
status VARCHAR(20) NOT NULL (pending/in_progress/approved/rejected/cancelled/completed)
context JSONB NOT NULL (trigger data + step results)
initiated_by UUID FK→users.id
created_at TIMESTAMPTZ NOT NULL
updated_at TIMESTAMPTZ NOT NULL
completed_at TIMESTAMPTZ NULL

Index: (tenant_id, status), (tenant_id, workflow_id), (tenant_id, initiated_by)

workflow_step_history (Audit Trail per Step)

Column Type Constraints
id UUID PK
tenant_id UUID FK→tenants.id
instance_id UUID FK→workflow_instances.id
step_index INTEGER NOT NULL
step_name VARCHAR(200) NOT NULL
action VARCHAR(50) NOT NULL (advance/approve/reject/cancel/timeout)
actor_id UUID FK→users.id NULL (NULL for system)
result JSONB NULL (step output data)
created_at TIMESTAMPTZ NOT NULL

Index: (tenant_id, instance_id, step_index)

Event Bus Integration

  • Workflows mit trigger_type=event werden automatisch gestartet, wenn das konfigurierte Event auf dem Event Bus veröffentlicht wird.
  • Workflow-Schritte können Events emitten (z.B. workflow.step_completed, workflow.approved).
  • Der Event Bus dispatcht Events an registrierte Workflow-Handler.

Code-Engine Workflows

Hartkodierte Workflows leben in app/workflows/code/:

  • onboarding.py — User-Onboarding-Sequence (Default-Rolle, Default-Kalender, Welcome-Mail)
  • plugin_sequence.py — Plugin-Activation-Sequenz (migrations → activate → notify)
  • mail_sync_trigger.py — Trigger IMAP-Sync beim Account-Setup

Code-Workflows verwenden denselben workflow_instances + workflow_step_history tables für Audit-Trail.


8d. Monitoring & Alerting (F-INFRA-04)

Health Endpoint (erweitert)

GET /api/v1/health gibt strukturierten Status zurück:

{
  "status": "healthy",
  "checks": {
    "database": {"status": "up", "latency_ms": 2.3},
    "redis": {"status": "up", "latency_ms": 0.8},
    "storage": {"status": "up"},
    "worker": {"status": "up", "queue_depth": 3}
  },
  "version": "1.0.0",
  "uptime_seconds": 86400
}
}

Bei Degradation: "status": "degraded" mit betroffenen Checks markiert als "down".

Prometheus Metrics Endpoint

GET /api/v1/metrics (Prometheus format, admin-only auth):

  • leocrm_http_requests_total{method, path, status} — Request counter
  • leocrm_http_request_duration_seconds{method, path} — Histogram
  • leocrm_db_pool_connections{state} — DB pool gauge (idle/used/overflow)
  • leocrm_redis_pool_connections{state} — Redis pool gauge
  • leocrm_arq_jobs_total{queue, status} — Job counter
  • leocrm_tenant_active_sessions{tenant_id} — Active session gauge

Alerting

  • DB-Connection-Pool erschöpft: Warning-Alert via structured log (ALERT: db_pool_exhausted)
  • App down: Coolify healthcheck erkennt status=unhealthy → Auto-Restart
  • Backup-Fehler: ARQ job failure → Notification an Admin-User + Error-Log
  • Worker-Queue-Depth > 100: Warning-Log ALERT: worker_queue_backlog
  • Response-Zeit > 2s: Slow-request-log mit Request-Details

Structured Logging (F-INFRA-03)

Alle Logs sind JSON-formatiert (structlog):

{"timestamp": "2026-06-28T13:00:00Z", "level": "INFO", "event": "http_request", "method": "GET", "path": "/api/v1/companies", "status": 200, "duration_ms": 45, "tenant_id": "...", "user_id": "..."}

Log-Level via LOG_LEVEL env var (DEBUG/INFO/WARNING/ERROR).


8e. Performance (F-PERF-01)

Requirements

  • API-Response <500ms für List-Endpunkte bei 200k Datensätzen
  • Full-Text-Search <500ms bei 200k Datensätzen
  • Frontend: Lazy Loading, Code-Splitting, minimal bundle size

DB Indexing Strategy

Alle Such- und Filter-Felder haben PostgreSQL-Indizes:

Table Index Type
companies (tenant_id, name) B-Tree
companies (tenant_id, industry) B-Tree
companies (tenant_id, billing_country) B-Tree
companies (tenant_id, rating) B-Tree
companies (tenant_id, deleted_at) B-Tree (partial: WHERE deleted_at IS NULL)
contacts (tenant_id, last_name) B-Tree
contacts (tenant_id, first_name) B-Tree
contacts (tenant_id, email) B-Tree
contacts (tenant_id, deleted_at) B-Tree (partial)
contacts GIN(tenant_id, body_tsv) GIN (FTS)
mails GIN(body_tsv) GIN (FTS)
mails (tenant_id, thread_id) B-Tree
calendar_entries (tenant_id, start_at) B-Tree
calendar_entries (tenant_id, due_date) B-Tree
audit_log (tenant_id, timestamp) B-Tree
audit_log (tenant_id, entity_type) B-Tree

Query Optimization

  • Pagination: Default page_size=25, max page_size=100. Keyset pagination für >10k Ergebnisse (?cursor=... statt OFFSET).
  • Eager Loading: SQLAlchemy selectinload() für N:M Beziehungen (vermeidet N+1 Queries).
  • Query Limits: Alle List-Queries haben LIMIT (max 100 rows per page). Full-Table-Scans verboten.
  • Export als Background-Job: Export >1000 Datensätze → ARQ background job → Notification bei Fertigstellung (F-SCHED-01).
  • Streaming-Response: CSV-Export streamed via StreamingResponse (kein full in-memory load).

Frontend Performance

  • Code-Splitting: Route-level React.lazy() + Suspense (minimal initial bundle).
  • Plugin Lazy-Load: Plugin UI components lazy-loaded.
  • TanStack Query Caching: Stale time 60s, Garbage collection 5min, Background refetch on focus.
  • Virtual Scrolling: Tabellen mit >1000 rows verwenden virtual scrolling (TanStack Virtual).
  • Image Optimization: User-Avatar via srcset + lazy loading.

Performance Tests

# Seed 200k contacts
python scripts/seed_perf_data.py --count 200000

# Benchmark list endpoint
curl -w '%{time_total}' 'http://localhost:8000/api/v1/contacts?page=1&page_size=25' -b cookie.txt

# Benchmark FTS search
curl -w '%{time_total}' 'http://localhost:8000/api/v1/contacts?search=Mueller' -b cookie.txt
# Expected: <500ms

9. Test Strategy (F-TEST-01)

v1 Core Feature — F-TEST-01: Testing-Strategie — pytest + httpx (backend), Vitest + Testing Library (frontend), Playwright (E2E).

Backend (pytest + httpx)

tests/
├── conftest.py              — Fixtures: test client, test DB, auth helpers, seed data
├── test_auth.py            — Login, logout, RBAC, password reset, tenant switch
├── test_companies.py       — CRUD, search, filter, pagination, soft-delete, N:M, audit
├── test_contacts.py        — CRUD, search, filter, N:M, DSGVO delete
├── test_tenant.py          — Tenant isolation, cross-tenant access
├── test_plugins.py         — Plugin install/activate/deactivate, event bus, migrations
├── test_dms.py             — Folders, files, upload, search, links, permissions, shares
├── test_calendar.py       — Entries, appointments, tasks, kanban, recurrence, ICS
├── test_mail.py            — Accounts, IMAP mock, send, search, threading, templates
├── test_import_export.py  — CSV import/export, Excel export
├── test_notifications.py   — Create, read, unread count
├── test_health.py          — Health endpoint
├── test_ai_copilot.py     — KI-Copilot API (query, execute, RBAC enforcement, history)
├── test_workflows.py       — Workflow CRUD, instances, steps, approval/rejection, event triggers
├── test_monitoring.py     — Extended health check, Prometheus metrics, alerting
└── test_performance.py    — 200k seed + list <500ms, FTS <500ms, export streaming

Coverage Target: >80% backend (measured via pytest-cov).

Frontend (Vitest + Testing Library)

frontend/src/__tests__/
├── components/             — UI component tests
├── features/               — Feature integration tests
└── hooks/                  — Custom hook tests

E2E (Playwright)

e2e/
├── auth.spec.ts            — Login → logout
├── company-crud.spec.ts    — Create → edit → delete company
├── contact-crud.spec.ts    — Create → link to company → delete
├── search.spec.ts          — Global search
└── plugin-toggle.spec.ts   — Activate/deactivate plugin

10. ADRs (Architecture Decision Records)

ADR-01: PostgreSQL 16 instead of SQLite

Context: Original requirements specified SQLite. System needs 200k records + multi-user concurrent writes.

Decision: PostgreSQL 16.

Rationale: SQLite has file-level locking (~15 updates/sec under concurrency). PostgreSQL uses MVCC (~1500 updates/sec). Full-Text-Search via tsvector + GIN indexes. JSONB support for flexible fields.

Alternatives: SQLite+WAL (insufficient concurrency), MySQL (no native FTS in same way), MongoDB (no relational integrity).

ADR-02: ARQ instead of Celery for Background Jobs

Context: Background jobs needed for exports, mail-sync, reminders, backups.

Decision: ARQ (async Redis-based job queue for Python).

Rationale: Native async (works with FastAPI/asyncio). Simpler than Celery. Redis already in stack for caching. No separate broker needed. Lightweight.

Alternatives: Celery (heavier, needs RabbitMQ or Redis as broker), FastAPI BackgroundTasks (no retry, no status tracking, no persistence).

ADR-03: Plugin System via Python Entry Points + Manifest

Context: Module system needed for Mail, Calendar, DMS, Tags as plugins.

Decision: Built-in plugins in app/plugins/builtins/ with manifest-driven registration. No dynamic external plugin loading in v1.

Rationale: All v1 plugins are built-in (shipped with the code). External plugin installation is post-MVP. Manifest provides metadata for UI registration and lifecycle hooks. Simpler and safer than dynamic imports.

Alternatives: Dynamic pip-installable plugins (complex, security risk for v1), Microservices (overkill for small team).

ADR-04: TanStack Query for Server State

Context: Frontend needs to manage API data with caching, loading states, error handling.

Decision: TanStack Query (React Query v5).

Rationale: Built-in caching, optimistic updates, background refetching, request deduplication. Eliminates need for global state manager for API data. Works with React 18 Suspense.

Alternatives: Redux Toolkit Query (heavier), SWR (less features), manual fetch+useEffect (no caching).

ADR-05: Session-based Auth instead of JWT

Context: Requirements specify session-based auth with HttpOnly cookies.

Decision: Server-side sessions in Redis (primary store for fast lookup), session ID in HttpOnly+Secure+SameSite=Strict cookie. PostgreSQL sessions table retains session records as an audit trail (created_at, expires_at, user_id, tenant_id).

Rationale: No JWT complexity (refresh tokens, rotation). Server can invalidate sessions immediately (delete Redis key). CSRF protection via SameSite=Strict + Origin validation. 8h timeout (Redis TTL). Redis for fast session lookup on every request; PostgreSQL sessions table as immutable audit trail for security analysis. Session validation flow: check Redis key → if expired/missing, check PostgreSQL audit record for forensic logging → deny request.

Alternatives: JWT (harder to invalidate, token leakage risk), OAuth2 (overkill for internal CRM).

ADR-06: Soft-Delete with deleted_at Column

Context: Companies and contacts need recoverable deletion.

Decision: deleted_at TIMESTAMPTZ NULL column. NULL = active, NOT NULL = soft-deleted.

Rationale: Simple, queryable, recoverable. ORM auto-filters deleted_at IS NULL on list endpoints. DSGVO hard-delete removes row entirely and logs to deletion_log.

Alternatives: Separate trash table (more complex), status column (less clear semantics).


Open Questions

  1. SMTP Provider (Production): Welcher Provider? → Deployment-Phase.
  2. Backup Strategy: pg_dump + S3 or Coolify volume backup? → Deployment-Phase.
  3. OnlyOffice: Separate container or embedded? → Separate container confirmed in docker-compose.
  4. Frontend Calendar Library: FullCalendar (heavy) or custom React implementation? → Implementation-Phase.

Documentation (F-DOC-01)

v1 Core Feature — F-DOC-01: Dokumentation — README.md, docs/admin-guide.md, docs/api-overview.md, Swagger UI at /api/v1/docs.

Handoff

  • Architecture status: COMPLETE
  • Task graph status: PENDING (see task_graph.json)
  • AGENTS.md status: PENDING
  • Open questions: 4 (listed above, non-blocking for implementation)
  • Ready for implementation: NO (pending quality_reviewer review + task graph + AGENTS.md)