Mandantenfähigkeit & Tenant-Isolation (OP-TENANT)
Status: Design festgezurrt (kein Code) — Entscheidungen 2026-06-27, alle E-TENANT-Sign-offs ✅ (§7); Umsetzung (Slices T-1…T-7, §6) offen · Bezug: OP-TENANT (Owning-OP) · OP-DATA-1 (
docs/architektur/Persistenz.md) · OP-AUDIT-1 (docs/architektur/Audit-Log.md) · OP-BACKUP-1 (docs/architektur/Backup-Restore.md) · OP-AUTH-1 (docs/architektur/Billing-Zugang.md) · Zielgruppe: IT-Dev/Architektur · CISO/Security · True North: nur indirekt — die Tenant-Isolation ist das Fundament dafür, dass jeder Betrieb die drei Leitfragen auf seinen eigenen Daten beantwortet bekommt (beantwortet selbst keine).
Kernaussage. Architektur der Mandantentrennung: Daten pro Tenant komplett (physisch) getrennt (Entscheidung des Auftraggebers, 2026-06-27) — DO-SQLite je Tenant, inklusive Audit-Log. Dieses Dokument zurrt die Datenhaltung je Datenklasse fest und benennt die zwei bewussten zentralen Ausnahmen: Identität/Registry via Better Auth und der zentrale Hash-Anker für Audit-Manipulationsnachweise (E-TENANT-1 = C, keine Events/PII zentral). Fotos in R2 sind logisch per Tenant-Prefix isoliert (nicht physisch wie die DO-SQLite-Daten).
Verhältnis zu Seed-Ablöse-Plan: dieses Doc = Zielarchitektur der Tenant-Trennung; der Seed-Ablöse-Plan = Migrationsweg dorthin (Seed aus dem Laufzeitpfad → Tenant-Vorlage + Purge). Beide teilen OP-TENANT.
1. Getroffene Entscheidungen (2026-06-27)
- Mandantendaten (heißer Arbeitssatz + Historie): Isolations-Option A — DO-SQLite je Tenant („Room =
Tenant"). Physische Trennung durch die Durable-Object-Grenze; keine geteilte Abfrageoberfläche.
→ löst zugleich E-DATA-1 = B (DO-SQLite,
docs/architektur/Persistenz.md): Option A der Isolation ist die DO-SQLite-Persistenz. Beide Fragen damit beantwortet. - Audit-Log: ebenfalls Option A — je Tenant in dessen DO-SQLite (statt geteilter D1). Maximale Isolation + Audit-PITR je Tenant. Mitigation der Compliance-Folge: siehe §4.
- Tenant-Registry: zentral — über Better Auth (Organizations) und dessen EU-D1 (OP-AUTH-1,
docs/architektur/Billing-Zugang.md). Die einzige bewusst zentrale Datenklasse, weil sie die Identität→Tenant- Zuordnung ist (Enforcement-Quelle, §5) und naturgemäß mandantenübergreifend.
2. Leitprinzip
Physische Trennung je Tenant ist der Default; Zentralität ist die begründete Ausnahme. Alles, was zu einem Mandanten gehört, lebt in dessen DO (DO-SQLite). Zentral bleibt nur, was die Mandanten erst zuordnet (Identität/Registry) — und, als bewusste minimale Ausnahme (E-TENANT-1=C, §4), ein zentraler Hash-Anker (nur der Chain-Kopf, keine Daten) für die off-box-Manipulationssicherheit des Audits.
3. Datenhaltung je Datenklasse
| Datenklasse | Heimat | Trennung | Begründung |
|---|---|---|---|
Heißer Arbeitssatz (auftraege aktiv, belegung, Stammdaten) | DO-SQLite je Tenant | physisch | Single-Writer-DO, kein Cross-Query-Surface |
Wachsende Historie (leistungsnachweise, Abwesenheits-/Beförderungs-Historie, archivierte Aufträge) | DO-SQLite je Tenant (Tabellen, on-demand gefiltert) | physisch | beseitigt Blob-Wachstum, abfragbar, PITR je Tenant |
| Audit-Log | DO-SQLite je Tenant (primär, append-only + Hash-Chain) + zentraler Hash-Anker (nur Chain-Kopf, keine Events/PII, §4) | physisch | volle Isolation + off-box-Tamper-Nachweis ohne zentrale Daten (E-TENANT-1=C) |
| Fotos/Blobs (R2) | ein EU-Bucket, Key-Prefix tenant/<id>/…, gekapselt hinter tenantBucket()/tenantKey() (§5) | logisch (E-TENANT-2=3) | opake Bytes; die Foto↔Tenant↔Auftrag-Relation liegt bereits physisch in der DO-SQLite; Prefix skaliert auf tausende kleine Mandanten (Bucket-Limit), Bucket-je-Tenant = Opt-in-Upgrade |
| Tenant-Registry / Identität / Seats | zentral (Better Auth, EU-D1) | zentral (gewollt) | ordnet Identität→Tenant zu; Enforcement-Quelle |
| Cross-Tenant-Reporting (Audit-Aggregat, Betriebs-Analytics) | Fan-out über die Tenant-DOs (Sink = Upgrade C→A) | abgeleitet | keine eigene geteilte Live-DB für Mandantendaten |
4. Audit-Log: Compliance-Folge der „Option A"-Entscheidung (proaktiv geflaggt)
Spannungsfeld. Die bisherige OP-AUDIT-1-Architektur legt das Audit-Log bewusst in eine separate D1
(taktano-audit, heute live, Dual-Write Blob+D1). Das gab zwei Eigenschaften: (a) der Live-Writer kann
das Log physisch nicht ändern (Gewaltenteilung → „nicht reverse-engineerbar") und (b) mühelose
Cross-Tenant-Compliance-Sicht. Audit in die per-Tenant-DO-SQLite zu verlegen, gibt volle Isolation +
PITR, verliert aber zunächst (a) und (b).
E-TENANT-1 ✅ ENTSCHIEDEN 2026-06-27: (C) Zentraler Hash-Anker (kein voller Sink)
Erwogen wurden drei Stufen: (A) voller zentraler Audit-Sink (alle Events PII-gescrubbt in D1) · (B) kein Zentrales (rein per-Tenant + Fan-out) · (C) nur ein zentraler Hash-Anker je Tenant. Gewählt: (C) — ehrt „komplett getrennt" am stärksten und schließt trotzdem die Manipulationssicherheits- Lücke, ohne zentrale Events/PII.
Mechanik (C):
- Primär (je Tenant): Audit als append-only Tabelle in der Tenant-DO-SQLite — im Code kein
UPDATE/DELETE(nur Retention-Purge-Job), plus Hash-Chain:eintrag_n.hash = H(payload_n ‖ eintrag_{n-1}.hash). Jede nachträgliche Einfügung/Löschung/Änderung bricht die nachgerechnete Kette. - Zentral (off-box, minimal): periodisch/forward-only wird nur der Chain-Kopf je Tenant verankert —
{ tenant_id, seq, head_hash, ts }(optional signiert / mit externem Zeitstempel) in der umgewidmetentaktano-audit-D1. Keine Events, keine PII zentral. Verifikation = Tenant-Log neu durchhashen und gegen die verankerten Köpfe prüfen → off-box-Manipulationsnachweis ohne zentrale Datenhaltung. - Cross-Tenant-Reporting bleibt Fan-out über die Tenant-DOs (bewusst akzeptiert). Upgrade-Pfad C→A: falls mandantenübergreifendes Audit-Reporting später ein echter Betreiber-Bedarf wird, kann der Anker zum vollen Sink erweitert werden — der Hash-Anker ist die saubere Zwischenstufe.
Folge für den bestehenden Code: Der heute live geschaltete D1-Dual-Write (#186) schreibt aktuell volle
Events nach D1 — das ist Interim und nutzbar. End-Zustand (C) reduziert die zentrale Schreibmenge
auf den Hash-Kopf (Events bleiben nur per-Tenant). taktano-audit wird also vom „Audit-Primärspeicher"
zum zentralen Tamper-Anker umgewidmet (Tabelle audit_anchors neben/statt audit_entries). Migration =
OP-AUDIT-1/OP-TENANT-Folge-Slice (T-3/T-4), kein Sofort-Rip-out.
5. Enforcement — physische Trennung nützt nur mit vertrauenswürdigem Routing
- Tenant-Id aus der verifizierten Identität ableiten (Better-Auth-Organizations-Claim / Access-JWT)
→ daraus den DO-Roomnamen bestimmen. Nie client-gesetzt (Muster wie heute beim
kostenAdmin-Gate: server-seitig aus der vom Worker injizierten, verifizierten Identität). - Heute: fixer Room
leitstand-vest. OP-TENANT macht den Room identitätsabgeleitet. - Weitere Leck-Flächen mit
tenant_idscopen: Logs (trace_id+tenant_id, OP-LOG-1), Backups (Snapshots/PITR je Tenant, OP-BACKUP-1). In-Memory unkritisch (ein DO = ein Tenant). - R2 (E-TENANT-2=3): ausschließlich über
tenantBucket()/tenantKey(tenantId, …)(Prefixtenant/<id>/…) zugreifen — nie rohe Keys; unrätbare Foto-Ids + signierte URLs zur Auslieferung;tenant_idimmer aus der verifizierten Identität. Die Kapselung macht den späteren Bucket-je-Tenant-Split (Opt-in für High-Compliance-Tenants) zu einer mechanischen Änderung an einer Stelle.
6. Slices (OP-TENANT)
- T-1 · Tenant-Auflösung im Worker. Identität→
tenant_id→DO-Room (statt fixleitstand-vest); Registry-Lookup gegen Better Auth. Kein Datenschema-Change. - T-2 · DO-SQLite je Tenant (= OP-DATA-1 Slice D5-3): Live-State + Historie von Blob → DO-SQLite
(Drizzle), Provisionierung = neuer Room +
baueState(gewählte Vorlage)(S-SEED-4-Vorlage). - T-3 · Audit → DO-SQLite je Tenant + Hash-Chain; T-4 · zentraler Hash-Anker (E-TENANT-1=C):
forward-only Push des Chain-Kopfs
{tenant_id, seq, head_hash, ts}intaktano-audit(umgewidmet zuaudit_anchors, keine Events/PII) + Verifikations-Pfad (Tenant-Log neu durchhashen vs. Anker). - T-5 · Cross-Tenant-Reporting per Fan-out (Sink = Upgrade C→A nur bei echtem Bedarf) + Betreiber-Sicht.
- T-6 · Isolations-Tests (CI: kein Cross-Tenant-Read möglich) + Audit-Tamper-Evidenz-Test.
- T-7 · R2-Kapselung (E-TENANT-2=3):
tenantBucket()/tenantKey()+ signierte URLs + unrätbare Ids; bestehendetenant/<id>/-Prefix-Konvention durchsetzen. Bucket-je-Tenant = späteres Opt-in.
7. Sign-offs
- E-TENANT-1 ✅ entschieden 06-27 = (C) zentraler Hash-Anker (kein voller Sink) — §4.
- E-TENANT-2 ✅ entschieden 06-27 = (3) ein Bucket + Prefix je Tenant, gekapselt (Bucket-je-Tenant = Opt-in-Upgrade) — Begründung: relationale Isolation liegt schon in der DO-SQLite; Prefix skaliert auf tausende kleine Mandanten (R2-Bucket-Limit); §3/§5.
- E-TENANT-3 ✅ entschieden 06-27 = zurückgestellt („später, auf Anforderung"): per-Tenant-Ver- schlüsselung at-rest (Envelope, eigene Keys) wird nicht vorab gebaut — erst wenn ein konkreter Compliance-/Vertragsbedarf sie fordert. Bis dahin gilt die Plattform-Verschlüsselung at-rest (DO/D1/R2 in EU) als ausreichend. Re-evaluieren bei Enterprise-/regulatorischen Anforderungen (OP-COMPLIANCE-1).
➡ Alle E-TENANT-Sign-offs abgeschlossen (E-DATA-1=B · E-TENANT-1=C · E-TENANT-2=3 · E-TENANT-3=zurück-
gestellt). Offen bleiben nur die OP-BACKUP-1-Detailfragen (E-BACKUP-1/2/3, docs/architektur/Backup-Restore.md).
8. Bezüge
OP-DATA-1 (docs/architektur/Persistenz.md — E-DATA-1 = B/DO-SQLite hiermit entschieden) · OP-AUDIT-1
(docs/architektur/Audit-Log.md — Speicher-Entscheidung wird per §4 verfeinert) · OP-BACKUP-1
(docs/architektur/Backup-Restore.md — PITR je Tenant) · OP-AUTH-1/2 (docs/architektur/Billing-Zugang.md — Better Auth
Organizations = Registry/Enforcement) · OP-SEED-1/OP-TENANT-Vorlage (docs/architektur/Seed-Abloese-Plan.md —
baueState + benannte Vorlage = Tenant-Provisionierung) · OP-COMPLIANCE-1 (DSGVO/EU-Residenz).
↩ Zurück zur Doku-Landkarte · Lesepfade · Register (alle OPs)