Persistenz-Konvention & Migrations-Hygiene (OP-DATA-1 / S-SEED-5)
Status: weitgehend umgesetzt — Slices D5-1 (✅ 0.16.0) · D5-2 (✅ 0.17.0) · D5-4 (✅ 0.18.0, s. §5); D5-3 (DO-SQLite, E-DATA-1 = B) ist angelaufen — die erste DO-SQLite-Runtime-Tabelle
metrik_snapshots(✅ 0.146.0) validiert die Mechanik (Tabelle + Drizzle-Schema-Beweis + Broadcast-Integration) live; offen bleibt der Kern (Migration einer bestehenden State-Klasse wie Aufträge Blob→DO-SQLite) · Bezug: OP-DATA-1 (Owning-OP) / S-SEED-5 · OP-TENANT · OP-AUDIT-1 · OP-BACKUP-1 · Zielgruppe: IT-Dev/Architektur · CISO/Security · True North: nur indirekt — saubere Persistenz/Migrationen sichern die Datenbasis, aus der die drei Leitfragen beantwortet werden (beantwortet selbst keine).
Kernaussage. Die Datenhaltung vereinheitlichen: Drizzle-Schema als Single Source → generierte Migrationen + CI-Gate (CLAUDE.md §Datenmodell), statt der handgepflegten
onStart-Remaps. Kernfrage: Wo lebt welcher Datentyp — DO-Blob · DO-SQLite · D1? Dieses Dokument zurrt die Konvention fest. Auslöser: letzter Slice des Seed-Ablöse-Plans (docs/architektur/Seed-Abloese-Plan.md§4, S-SEED-5).
1. Ist-Zustand (Stand 0.146.0)
| Datenklasse | Ort heute | Migration heute |
|---|---|---|
| Live-Betriebszustand (Aufträge, Belegung, Stammdaten-State, Vorschlags-/Foto-/Historien-Listen) | DO-Blob — ein JSON unter STORAGE_KEY = "leitstand.state" je Durable Object (leitstand-vest) | SCHEMA_VERSION (aktuell 15, manuell hochgezählt). Versionierte Shape-Remaps seit D5-2 in der getesteten Registry server/src/persistence/do-state-migrations.ts (migriereDoState, Round-Trip-Self-Test in CI). Feld-Default-Merges (saved.feldX ?? this.state.feldX) + emptiness-gegatete Backfills bleiben idempotent in onStart (noch nicht coverage-gegated → D5-4). |
| Audit-Log (append-only Events, OP-AUDIT-1) | D1 (taktano-audit, Binding DB, EU weur) | raw-SQL Migration server/migrations/0001_audit_entries.sql, angewandt über wrangler/migrations_dir. Kein Drizzle. Schema ist bereits tenant_id-fähig. |
| Metrik-Verlauf (Tages-Snapshots der KPIs, OP-AUDIT-1/OP-COST-1) | DO-SQLite — Tabelle metrik_snapshots im DO selbst (ctx.storage.sql), tenant-lokal | Erste DO-SQLite-Runtime-Tabelle (Vorbote von D5-3). Drizzle-Schema server/src/db/schema.ts = Single Source (im db:check-Gate), Runtime-Anlage CREATE TABLE IF NOT EXISTS in onStart (idempotent, analog Audit-Runtime). tenant_id+tag = PK; ein Snapshot/Tag; Retention 400 Tage. |
| Fotos (OP-R4-3) | R2 (taktano-fotos, EU) | n/a (Objektspeicher) |
Schmerzpunkt (S-SEED-5). Jedes neue State-Feld erfordert einen von Hand geschriebenen Backfill in
onStart + ein SCHEMA_VERSION-Inkrement (genau das geschah in S-SEED-1…3 für arbeitsplaetze,
taxonomy, schichten). Es gibt keine Garantie, dass Schema und Migration synchron sind — ein neues
Feld ohne Backfill fällt erst zur Laufzeit auf. Genau diese Lücke schließt OP-DATA-1: deklaratives Schema +
generierte Migrationen + CI-Gate.
2. Die drei möglichen „Heimaten" für Daten
- DO-Blob (heute): ein serialisiertes JSON im DO-Storage. Pro: minimal, atomar mit der In-Memory- Arbeitskopie, ideal für den WebSocket-Broadcast-Loop. Contra: nicht abfragbar (kein SQL), Migration nur imperativ, wächst mit dem Mandanten (Blob-Größe/Serialisierungskosten).
- DO-SQLite (Cloudflare DO mit SQLite-Storage-Backend): relationale Tabellen im DO selbst. Pro:
abfragbar, co-lokal je DO/Mandant (passt zu „Room = Tenant", OP-TENANT), transaktional, Drizzle-
migrierbar (
drizzle-orm+drizzle-kitgegen DO-SQLite). Contra: Architektur-Umbau des State-Layers; neuere CF-Fähigkeit (Reifegrad/Migrationspfad prüfen). - D1 (separate, globale SQL-DB): bereits für das Audit-Log genutzt. Pro: abfragbar, cross-tenant
(Analytics/Audit/Registry), unabhängig vom DO-Lebenszyklus. Contra: zweiter Storage neben dem DO,
Cross-Tenant per Konvention/
tenant_id(Isolation = Disziplin, nicht physisch), Latenz/Konsistenz ggü. dem Live-State.
3. Leitkonvention (Empfehlung) — Daten nach Charakter, nicht pauschal
Nicht „alles nach SQL", sondern je Datencharakter die passende Heimat:
- Flüchtiger/heißer Live-Zustand (der WS-Broadcast-Loop: aktueller Plan, Belegung, transiente Vorschau/Vorschläge) → bleibt im DO (Blob heute; Kandidat für DO-SQLite, sobald abfragbar nötig wird). Realtime-Kollaboration ist genau das, wofür der DO da ist.
- Append-only / revisionssicher (Audit-Events, später Backup-Snapshots) → primär tenant-lokal
(DO-SQLite je Tenant: Isolation + Tamper-Evidenz + PITR, vgl.
docs/architektur/Mandantenfaehigkeit.md§4); D1 (EU-Residenz) zentral nur als Tamper-Anker (Hash-Chain-Kopf, kein voller Event-Sink, keine PII zentral — ✅ E-TENANT-1 = Variante C, entschieden 2026-06-27, Detail + Upgrade-Pfad C→A:docs/architektur/Mandantenfaehigkeit.md§4) und für die Tenant-Registry. Heute noch D1-Dual-Write des Audit-Logs (#186), bis die Tenant-Slices greifen. - Strukturierte, wachsende Stammdaten (Mitarbeiter, Arbeitsplätze, Taxonomie, Schichten, Aufträge) →
DO-SQLite (co-lokal, abfragbar) — entschieden 06-27 (E-DATA-1=B, §4 /
docs/architektur/Mandantenfaehigkeit.md).
Querschnitt (unabhängig von der Heimat): Drizzle ist die Single Source des SQL-Schemas (D1 und
ggf. DO-SQLite). Migrationen werden generiert (drizzle-kit generate), nicht handgeschrieben; ein
CI-Gate erzwingt Schema↔Migrations-Synchronität.
4. Entscheidung
E-DATA-1 — Heimat der strukturierten Stammdaten/Aufträge → ✅ ENTSCHIEDEN 2026-06-27: (B) DO-SQLite je
Tenant. Folgt direkt aus der Tenant-Isolations-Entscheidung „Option A" (docs/architektur/Mandantenfaehigkeit.md):
Daten pro Tenant physisch im jeweiligen Durable Object → DO-SQLite ist Speicher und Isolation in
einem. Migrationspfad = OP-DATA-1 Slices D5-1 (Drizzle-Toolchain auf der Audit-D1) → D5-3 (DO-SQLite-Pilot)
→ Voll-Migration. Die drei ursprünglich erwogenen Wege bleiben als Begründung dokumentiert:
- (A) DO-Blob behalten, nur Migrations-Hygiene verbessern. Kleinster Eingriff: die ad-hoc
onStart-Remaps in eine registrierte, getestete, geordnete Migrations-Liste überführen (Versionsnummer + benannte Migrationsfunktionen + Self-Test, der jede Version round-trippt) + CI-Coverage-Gate „neues Feld ⇒ Migration vorhanden". Kein Storage-Wechsel, kein SQL für den Live-State. Empfohlen als Sofort-Schritt. - (B) DO-SQLite einführen. Live-State von Blob → DO-eigenes SQLite, Drizzle-Schema + generierte Migrationen, co-lokal je Tenant. Beste Langfrist-Passung zu OP-TENANT (Room=Tenant) + macht den State abfragbar (True-North-Fragen direkt per SQL statt Projektion). Größter Umbau.
- (C) D1 für Stammdaten. Stammdaten in die globale D1 (mit
tenant_id). Gut für Cross-Tenant-Sicht, aber entkoppelt vom Live-DO (Latenz/Konsistenz) und macht Isolation zur Disziplin. Am wenigsten passend für heißen Live-State.
Empfehlung: (A) jetzt (Hygiene + Toolchain ohne Risiko), (B) als Zielbild evaluieren, sobald OP-TENANT konkret wird (DO-SQLite + Drizzle co-lokal je Tenant). (C) nur für explizit cross-tenant Daten (Audit ✓, Tenant-Registry, Backup-Index).
5. Slices (inkrementell, risikoarm zuerst)
- D5-1 · Drizzle-Toolchain auf der bestehenden D1. ✅ gebaut (0.16.0).
drizzle-orm+drizzle-kitals devDependencies (kein Prod-Bundle/Audit-Surface — die Schema-Datei wird nur vondrizzle-kitkonsumiert, nicht vom Worker-Laufzeitpfad); das schon existierendeaudit_entries-Schema (raw0001) als Drizzle-Schema nachgezogen (server/src/db/schema.ts= Single Source) — Schema-First, kein Daten-/Verhaltens-Change.server/drizzle.config.ts, generierte Migration (server/drizzle/) deckungsgleich zu0001(gleiche Tabelle/Spalten/Nullability/Indizes). Bewusst getrennt vom wrangler-migrations_dir→ wrangler liest weiter nur die remote angewandte, idempotente raw-0001(kein Doppel-Apply); das Drizzle-Pendant ist der maschinen-geprüfte Schema-Beweis. CI-Jobdb-check(npm run db:check→scripts/check-drizzle.sh:drizzle-kit generate→git statusmuss leer sein; fängt getrackte und neue Migrationsdateien), als Required-Gate inci-gateverdrahtet. - D5-2 · DO-Blob-Migrations-Registry (Variante A). ✅ gebaut (0.17.0). Die versionsgegateten
onStart-Remaps (v2/3/4/5/7/12/14) in eine geordnete, getestete, benannte Registry extrahiert (server/src/persistence/do-state-migrations.ts:DO_MIGRATIONS+migriereDoState+SCHEMA_VERSIONals Single Source) + Round-Trip-Self-Testtest:do-migrations(Voll-Migration v0→v15 · Idempotenz · Versions-Gating) in CI. Verhaltenstreu, kein Storage-Wechsel:leitstand.tsruft nur nochmigriereDoState; der emptiness-gegatete Abwesenheits-Historie-Backfill und die idempotenten Feld-Default-Merges bleiben bewusst inonStart(nicht versionsgegatet). Das Token-Muster-Gate („PersistedState-Feld hinzugefügt ⇒ Migration/Default vorhanden“) wurde nach D5-4 verschoben (jetzt ✅ dort). - D5-3 · Pilot DO-SQLite (E-DATA-1 = B) — angelaufen (v0.146.0, erweitert v0.153.0). Die erste
DO-SQLite-Runtime-Tabelle
metrik_snapshots(Metrik-Verlauf, OP-AUDIT-1/OP-COST-1; s. §1) hat die Mechanik live validiert: Runtime-Tabellen-Anlage (ctx.storage.sql+CREATE TABLE IF NOT EXISTS), Drizzle als Schema-Beweis (db:check) und Broadcast-Integration (metrikVerlaufim State). Mit v0.153.0 auch additive Spalten-Migration zur Laufzeit erprobt (Termintreue/Ø Durchlaufzeit:PRAGMA table_info→ fehlende Spalten perALTER TABLE ADD COLUMN; gen. Migrationdrizzle/0002_*.sql= Schema-Beweis) — fünf KPI-Spalten (auslastung_pct/fpy_pct/nacharbeit_pct/termintreue_pct/durchlaufzeit_min). Offen bleibt der Pilot-Kern: eine bestehende State-Klasse (z. B. Aufträge) von Blob → DO-SQLite migrieren + Latenz/Migrationspfad messen, dann über die Voll-Umstellung (B) entscheiden. - D5-4 · CI-Gate vereinheitlichen. ✅ gebaut (0.18.0). Ein gebündeltes Persistenz-Gate analog
scripts/check-no-hex.sh(scripts/check-persistence.sh, npmpersistence:check, CI-Jobpersistence-gateals Required-Gate inci-gate) — vereint (1) Drizzle-Schema-Staleness (D5-1,check-drizzle.sh) und (2) das DO-State-Migrations-Coverage-/Token-Muster-Gate (check-do-state-coverage.mjs): jedes nicht-optionalePersistedState-Feld braucht einen explizitensaved.<feld> ??-onStart-Default — sonst bricht CI (statt erst die Laufzeit, der S-SEED-5-Schmerzpunkt). Damit ist OP-DATA-1 bis auf den DO-SQLite-Pilot-Kern (D5-3, Mechanik seit 0.146.0 live validiert — s. o.) umgesetzt; der früheredb-check-Job ist impersistence-gateaufgegangen.
6. Leitplanken
- EU-Datenresidenz (DE/AT/CH, OP-COMPLIANCE-1): D1
weur/eu, DO-Region-Pinning prüfen; keine PII in Klartext-Logs (OP-LOG-1), PII-Scrubbing indelta_json(schon Audit-Konvention). tenant_idfirst-class in jedem SQL-Schema (Audit hat es bereits) — Voraussetzung für OP-TENANT-Isolation.- Derived State nie persistieren (CLAUDE.md §Datenmodell) — gilt unverändert, egal welche Heimat.
- Kein Doppel-Apply: remote bereits angewandte Migrationen (wie
0001) müssen idempotent bleiben.
7. Abgrenzung & Bezüge
S-SEED-5 ist kein Teil der Seed-Ablösung mehr (die ist mit S-SEED-4 abgeschlossen) — es ist
Persistenz-/Migrations-Hygiene. Eng verbunden: OP-AUDIT-1 (D1 live, raw-SQL — D5-1 hat Drizzle als Single Source nachgezogen),
OP-BACKUP-1 (docs/architektur/Backup-Restore.md — Snapshots brauchen eine definierte Heimat), OP-TENANT
(DO-SQLite co-lokal = Isolations-Fundament), OP-EXPORT-1, OP-COMPLIANCE-1. Tracking: HANDOFF §4 (OP-DATA-1).
↩ Zurück zur Doku-Landkarte · Lesepfade · Register (alle OPs)