diff --git a/BACKUP.md b/BACKUP.md new file mode 100644 index 0000000..42a9341 --- /dev/null +++ b/BACKUP.md @@ -0,0 +1,69 @@ +# RAPPORT-HOST — Backup-Konzept + +> Status: **Konzept + Datenmodell stehen** (Migration `0004_backups.sql`). Die +> Erzeugungs-/Restore-Logik wird gebaut, sobald echtes Provisioning gegen einen +> laufenden Rapport-Stack läuft — vorher gäbe es nur leere Listen und Buttons +> ohne Funktion. + +## 1 · Warum nicht jetzt schon „fertig" + +Im aktuellen **MOCK-Modus** existieren keine echten Kunden-Datenbanken — Instanzen +sind synthetische DB-Einträge. Ein Backup-Mechanismus braucht aber echte +Studio-Daten in einem laufenden Postgres. Darum: Konzept + `backups`-Tabelle +jetzt, ausführende Logik mit echtem Stack. + +## 2 · Was ein Backup im Modell A bedeutet + +Wir hosten **Modell A**: ein geteilter Rapport-Stack, jeder Kunde ist ein +*Studio* (mandantengetrennt per RLS). Ein „Backup von Kunde X" ist deshalb +**kein** Full-DB-Dump, sondern ein **studio-bezogener Export**: + +- alle Zeilen mit `studio_id = ` aus allen relevanten Tabellen +- plus die zugehörigen Storage-Objekte (Quittungen/Logos) des Studios + +Das hält Backups klein und verhindert, dass Kundendaten vermischt werden. + +## 3 · Erzeugung (geplant) + +| Aspekt | Entscheidung | +|---|---| +| Werkzeug | `pg_dump` mit studio-gefiltertem Export bzw. eine `export_studio(studio_id)`-RPC im Rapport-Schema | +| Zeitplan | täglich automatisch (cron im HOST-Backend) + manuell per Admin-Button | +| Speicherort | konfigurierbar via Env: lokale Disk (Default) oder S3/Backblaze | +| Verschlüsselung | at-rest (Storage-seitig) — bei externem Ziel Pflicht | +| Aufbewahrung | z.B. 7 tägliche + 4 wöchentliche (rotierend); konfigurierbar | +| Register | jede Datei wird in `backups` protokolliert (Status, Größe, sha256) | + +## 4 · Restore (geplant, heikel) + +Restore in einem Multi-Tenant-Stack ist die gefährlichste Operation. Regeln: + +1. **Nur das eine Studio** wiederherstellen — niemals die ganze DB. +2. Vor Restore: **Sicherungs-Dump des aktuellen Studio-Stands** (Rollback-Option). +3. Restore in eine **Staging-Tabelle/Transaktion**, dann atomar umschalten. +4. Admin-Bestätigung mit Tippen des Kundennamens (kein Ein-Klick-Restore). +5. Audit-Log-Eintrag (wer/wann/welches Backup). + +## 5 · Datenmodell (`backups`-Tabelle, schon angelegt) + +`id · instance_id · account_id · trigger(scheduled|manual) · status(pending|ok|failed) +· storage_key · size_bytes · sha256 · error · created_at · completed_at` + +Die Dump-**Dateien** liegen NICHT in der DB, nur die Metadaten. Die Admin-UI +liest später nur diese Tabelle (Liste pro Kunde) + löst manuelle Backups/Restores +über Endpunkte aus. + +## 6 · Geplante API (noch nicht gebaut) + +``` +GET /api/admin/accounts/:id/backups Liste der Backups eines Kunden +POST /api/admin/instances/:id/backup manuelles Backup auslösen +POST /api/admin/backups/:id/restore Restore (mit Bestätigung) +GET /api/admin/backups/:id/download Dump herunterladen (signiert) +``` + +## 7 · Nächster Schritt + +Erst **echtes Provisioning** (laufender Stack, echte Studio-Daten). Dann: +`export_studio`/`import_studio`-RPC im Rapport-Schema → HOST-Cron + Endpunkte → +Admin-UI (Backup-Liste im Kunden-Detail + manuelles Backup/Restore). diff --git a/server/migrations/0004_backups.sql b/server/migrations/0004_backups.sql new file mode 100644 index 0000000..6e43003 --- /dev/null +++ b/server/migrations/0004_backups.sql @@ -0,0 +1,29 @@ +-- RAPPORT-HOST — Backup-Register. +-- Speichert NUR Metadaten zu Backups (eine Zeile pro erstelltem Dump). Die +-- eigentlichen Dump-Dateien liegen NICHT in dieser DB, sondern am Storage-Ort +-- (lokale Disk / S3 / Backblaze) — siehe BACKUP.md. +-- +-- Der Erzeugungs- und Restore-Mechanismus selbst (pg_dump pro Studio gegen den +-- Rapport-Stack) wird erst gebaut, wenn echtes Provisioning steht. Diese +-- Tabelle ist die Grundlage, damit die Admin-UI später nur noch lesen/anzeigen +-- muss. + +create table if not exists backups ( + id uuid primary key default gen_random_uuid(), + instance_id uuid references instances(id) on delete set null, + account_id uuid references accounts(id) on delete set null, + -- Wofür: automatischer Zeitplan oder manuell vom Admin ausgelöst. + trigger text not null default 'scheduled', -- scheduled | manual + status text not null default 'pending', -- pending | ok | failed + -- Wo die Datei liegt (Pfad/Key am Storage-Ort) + Größe + Prüfsumme. + storage_key text, + size_bytes bigint, + sha256 text, + error text, -- Fehlertext, falls failed + created_at timestamptz not null default now(), + completed_at timestamptz +); + +create index if not exists idx_backups_instance on backups(instance_id); +create index if not exists idx_backups_account on backups(account_id); +create index if not exists idx_backups_created on backups(created_at desc);