Zum Hauptinhalt springen

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)

DatenklasseOrt heuteMigration 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-lokalErste 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

  1. 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).
  2. 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-kit gegen DO-SQLite). Contra: Architektur-Umbau des State-Layers; neuere CF-Fähigkeit (Reifegrad/Migrationspfad prüfen).
  3. 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-kit als devDependencies (kein Prod-Bundle/Audit-Surface — die Schema-Datei wird nur von drizzle-kit konsumiert, nicht vom Worker-Laufzeitpfad); das schon existierende audit_entries-Schema (raw 0001) 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 zu 0001 (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-Job db-check (npm run db:checkscripts/check-drizzle.sh: drizzle-kit generategit status muss leer sein; fängt getrackte und neue Migrationsdateien), als Required-Gate in ci-gate verdrahtet.
  • 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_VERSION als Single Source) + Round-Trip-Self-Test test:do-migrations (Voll-Migration v0→v15 · Idempotenz · Versions-Gating) in CI. Verhaltenstreu, kein Storage-Wechsel: leitstand.ts ruft nur noch migriereDoState; der emptiness-gegatete Abwesenheits-Historie-Backfill und die idempotenten Feld-Default-Merges bleiben bewusst in onStart (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 (metrikVerlauf im State). Mit v0.153.0 auch additive Spalten-Migration zur Laufzeit erprobt (TermintreueDurchlaufzeit: PRAGMA table_info → fehlende Spalten per ALTER TABLE ADD COLUMN; gen. Migration drizzle/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, npm persistence:check, CI-Job persistence-gate als Required-Gate in ci-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-optionale PersistedState-Feld braucht einen expliziten saved.<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ühere db-check-Job ist im persistence-gate aufgegangen.

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 in delta_json (schon Audit-Konvention).
  • tenant_id first-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)