Workflow
Serie Vom MVP zur professionellen Lösung · Teil 1 von 6
Die Datenbank hinter dem Prototyp absichern
Code lässt sich neu generieren, Daten nicht. Welche Vorgaben dein Agent für Schema, Transaktionen und Webhooks braucht und was du selbst erledigen musst.
Warum die Daten der heikle Teil sind
Wenn ein Agent eine Funktion verpfuscht, lässt du sie neu bauen. Bei Daten geht das nicht. Eine Bestellung, die doppelt verbucht wurde, oder eine Spalte, die nach einer Änderung leer ist, bringt kein neuer Prompt zurück. Deshalb ist die Datenbank die Stelle, an der sich ein Prototyp am deutlichsten von einer Lösung unterscheidet, auf die sich jemand verlässt.
Das meiste davon kann dein Agent umsetzen, wenn du es ihm vorgibst. Die Prompts dafür stehen in den Abschnitten. Zwei Dinge gibst du nicht aus der Hand: das Zurückspielen eines Backups und die Rechte, mit denen der Agent an die echten Daten kommt. Dafür gibt es weiter unten je eine kurze Anleitung.
Die Beispiele nennen PostgreSQL, weil es weit verbreitet und gut dokumentiert ist. Die Prinzipien gelten für andere relationale Datenbanken genauso, die Befehle heißen dort anders.
Schemaänderungen als Migration
Das Schema ist der Bauplan der Datenbank: welche Tabellen es gibt, welche Spalten sie haben und wie sie zusammenhängen. Im Prototyp ändert der Agent diesen Bauplan direkt, sobald du ein neues Feld brauchst. Mit echten Daten darf jede Änderung nur noch als Migration passieren.
Eine Migration ist ein Skript, das genau eine Änderung beschreibt. Martin Fowler und Pramod Sadalage fassen die Regel so: Jede Änderung an der Datenbank wird als Migrationsskript festgehalten und zusammen mit dem Anwendungscode versioniert. Das Skript läuft zuerst lokal, dann in einer Testumgebung und zuletzt gegen die echte Datenbank. Der Grund steht dort auch: Zieht man Änderungen erst beim Ausliefern von Hand nach, weiß niemand mehr, wozu sie gedacht waren.
Für Nicht-Techniker heißt das: Zu jedem Stand der Datenbank gibt es eine nachlesbare Liste, wie er entstanden ist. Geht etwas schief, lässt sich sagen, welcher Schritt es war.
Die Regel gehört dauerhaft in die Anweisungsdatei des Agenten, also AGENTS.md oder das Gegenstück deines Werkzeugs:
Ändere das Datenbankschema ausschließlich über Migrationsdateien mit dem Migrationswerkzeug dieses Projekts. Führe niemals SQL direkt gegen eine Datenbank aus, um Tabellen oder Spalten zu ändern. Jede Migration enthält genau eine fachliche Änderung. Führe Migrationen nur gegen die lokale Datenbank oder die Testumgebung aus. Gegen die Produktionsdatenbank laufen sie ausschließlich über die Auslieferung, nie aus deiner Sitzung.
Ist der Prototyp ohne Migrationen entstanden, braucht es einmal einen Ausgangspunkt:
Dieses Projekt hat bisher keine Migrationsdateien. Prüfe, welches Migrationswerkzeug zum verwendeten Framework passt, und schlage es mir vor, bevor du etwas einrichtest. Erzeuge dann aus dem aktuellen Schema eine erste Migration als Ausgangsstand. Zeig mir, wie ich prüfen kann, dass eine leere Datenbank nach dem Einspielen aller Migrationen dasselbe Schema hat wie die bestehende.
Ob es stimmt, prüfst du an zwei Stellen. Im Repository liegt ein Ordner mit Migrationsdateien, und jede Schemaänderung der letzten Wochen taucht dort auf. Und eine frische, leere Datenbank lässt sich allein aus diesen Dateien aufbauen.
Schlüssel und Indizes
Ein Primärschlüssel ist der eindeutige Bezeichner einer Zeile. Ein Fremdschlüssel erzwingt, dass ein Verweis auf eine andere Tabelle dort auch ein Ziel hat, eine Bestellung also nicht auf einen Kunden zeigen kann, den es nicht gibt. Beides sind Regeln, die die Datenbank selbst durchsetzt, egal welcher Code gerade schreibt.
Ein Index beschleunigt die Suche. Die PostgreSQL-Dokumentation vergleicht ihn mit dem Stichwortverzeichnis eines Sachbuchs: Ohne ihn muss die Datenbank die ganze Tabelle Zeile für Zeile durchgehen. Dieselbe Dokumentation nennt den Preis. Jeder Index muss bei jeder Änderung mitgepflegt werden, selten genutzte Indizes sollen deshalb wieder weg. Es geht also um die Spalten, nach denen die Anwendung tatsächlich sucht und sortiert.
Das Tückische ist die Datenmenge. Mit hundert Testzeilen ist jede Abfrage schnell, ob mit Index oder ohne. Ob ein Index fehlt, zeigt sich erst mit einer Menge, wie sie nach einem Jahr Betrieb zu erwarten ist. PostgreSQL kann zu jeder Abfrage anzeigen, wie es sie ausführen wird. Der Befehl heißt EXPLAIN, und die Angabe "Seq Scan" bedeutet, dass die ganze Tabelle gelesen wird.
Prüfe das Datenbankschema und alle Abfragen dieser Anwendung. Arbeite nur mit der lokalen Datenbank.
- Liste jede Tabelle ohne Primärschlüssel und jede Spalte, die auf eine andere Tabelle verweist, aber keinen Fremdschlüssel hat.
- Fülle die lokale Datenbank über ein Skript mit realistischen Testdaten in der Größenordnung, die ich dir nenne.
- Führe für jede Abfrage, die die Anwendung im normalen Betrieb absetzt, EXPLAIN ANALYZE aus. Nenne mir jede Abfrage, die eine große Tabelle vollständig liest, und schlage den passenden Index vor.
- Nenne mir Indizes, die keine Abfrage nutzt.
Ändere nichts, bevor ich die Liste gesehen habe. Die Änderungen kommen danach als Migrationen.
Zur Größenordnung musst du etwas sagen können, und das ist eine fachliche Frage: Wie viele Kunden, Bestellungen oder Dokumente sind es in einem Jahr? Eine grobe Zahl genügt.
In denselben Durchgang gehört ein Blick auf Spalten, die ganze Datenstrukturen im JSON-Format enthalten. Das ist bequem, weil sich ohne Schemaänderung alles hineinlegen lässt. Die PostgreSQL-Dokumentation rät, solche Dokumente überschaubar zu halten: Jede Änderung sperrt die ganze Zeile, und ein Dokument sollte ein Datum abbilden, das sich fachlich nicht sinnvoll weiter zerlegen lässt. Was du abfragst, verknüpfst oder einzeln änderst, also Beträge, Zustände und Verweise, gehört in eigene Spalten.
Transaktionen und Sperren
Sobald ein Vorgang aus mehreren Schritten besteht, kann er mittendrin abbrechen. Eine Transaktion verhindert, dass dabei ein halber Zustand zurückbleibt: Sie bündelt die Schritte zu einem Vorgang, der ganz oder gar nicht stattfindet. Die PostgreSQL-Dokumentation erklärt das an einer Überweisung, bei der nicht abgebucht werden darf, ohne dass gutgeschrieben wird.
Vor einem zweiten Fall schützt sie allein nicht: dass zwei Anfragen gleichzeitig dasselbe tun. Liest der Code erst das Guthaben, prüft es und schreibt dann den neuen Stand, können zwei gleichzeitige Anfragen beide den alten Stand lesen und beide abbuchen. In der Standardeinstellung von PostgreSQL sieht eine einfache Leseabfrage, was vor ihrem Beginn bestätigt war, und nicht, was eine andere Transaktion gerade ändert. Dagegen hilft eine Zeilensperre: Mit SELECT ... FOR UPDATE reserviert sich eine Transaktion die Zeile, und jede andere wartet, bis die erste fertig ist.
Für Nicht-Techniker: Das ist der Unterschied zwischen "zwei Leute sehen denselben letzten Platz und buchen ihn beide" und "wer zuerst kommt, hält den Platz, bis er fertig ist".
Finde jede Stelle im Code, die Geld, Guthaben, Kontingente, Lagerbestand oder andere begrenzte Mengen verändert. Liste sie mit Datei und Funktion auf.
Prüfe für jede Stelle:
- Laufen alle zusammengehörigen Schreibvorgänge in einer Transaktion?
- Wird ein Wert erst gelesen und dann abhängig davon geschrieben? Wenn ja: Ist die Zeile dabei gesperrt, oder passiert die Änderung in einer einzigen Anweisung, die die Bedingung selbst prüft?
Schreib für jede Stelle einen Test, der dieselbe Aktion mehrfach gleichzeitig auslöst und prüft, dass das Ergebnis stimmt. Zeig mir zuerst die Tests, die fehlschlagen, und behebe die Ursachen erst danach.
Die Tests sind hier das Entscheidende. Ob eine Sperre fehlt, sieht man dem Code schwer an, ein Test mit zwanzig gleichzeitigen Abbuchungen von einem Guthaben, das für eine reicht, zeigt es sofort.
Doppelte Meldungen aushalten
Zahlungsanbieter und andere Dienste melden Ereignisse per Webhook, also mit einem automatischen Aufruf deiner Anwendung. Diese Meldungen sind nicht zuverlässig einmalig. Stripe schreibt in seiner Dokumentation: "Webhook endpoints might occasionally receive the same event more than once." Auf die Reihenfolge soll man sich laut derselben Seite ebenfalls nicht verlassen.
Die Verarbeitung muss deshalb idempotent sein, das heißt: Dieselbe Meldung zweimal zu verarbeiten hat dasselbe Ergebnis wie einmal. Der robuste Weg führt wieder über die Datenbank. Die Kennung jeder verarbeiteten Meldung wird in einer Tabelle gespeichert, und eine Eindeutigkeitsregel auf dieser Spalte sorgt dafür, dass der zweite Versuch scheitert, bevor etwas gutgeschrieben wird. Wichtig ist, dass das Speichern der Kennung und die Gutschrift in derselben Transaktion stehen.
Prüfe alle Webhook-Endpunkte dieser Anwendung.
- Wird die Signatur des Absenders geprüft, bevor irgendetwas verarbeitet wird?
- Wird die Kennung jedes verarbeiteten Ereignisses gespeichert, mit einer Eindeutigkeitsbedingung auf der Spalte?
- Stehen das Speichern der Kennung und die fachliche Änderung in derselben Transaktion?
- Funktioniert die Verarbeitung auch, wenn Ereignisse in anderer Reihenfolge ankommen als erwartet?
Schreib Tests, die dasselbe Ereignis zweimal nacheinander und zweimal gleichzeitig zustellen. Das Ergebnis muss jeweils eine einzige Gutschrift sein.
Verbindungen bündeln
Jede Verbindung zur Datenbank kostet dort Speicher, und ihre Zahl ist begrenzt. Das PostgreSQL-Wiki beschreibt, was passiert, wenn man sie ausreizt: Bis zu einem Sättigungspunkt steigt die Leistung mit der Zahl der Verbindungen, danach fällt sie. Mit weniger Verbindungen und einem Connection-Pool, der sie unter den Anfragen aufteilt, lassen sich oft mehr gleichzeitige Nutzer bedienen.
Besonders betrifft das Anwendungen auf Serverless-Plattformen, bei denen jede Anfrage in einer eigenen kurzlebigen Umgebung laufen kann und jede davon eine Verbindung öffnet. AWS empfiehlt für Lambda-Funktionen in Produktion deshalb einen vorgeschalteten Proxy, der einen Pool gemeinsamer Verbindungen verwaltet, damit hohe Parallelität die Datenbankverbindungen nicht erschöpft. Andere Plattformen und Datenbankanbieter haben vergleichbare Angebote, oft unter dem Namen Pooler.
Prüfe, wie diese Anwendung Verbindungen zur Datenbank öffnet. Wird pro Anfrage eine neue Verbindung aufgebaut, oder gibt es einen Pool, der beim Start einmal angelegt und wiederverwendet wird? Auf welcher Plattform läuft die Anwendung, und empfiehlt deren Dokumentation oder die des Datenbankanbieters einen vorgeschalteten Pooler? Verlinke mir die Stelle in der Dokumentation, statt aus dem Gedächtnis zu antworten. Nenne mir die höchste Zahl gleichzeitiger Verbindungen, die die Datenbank in unserem Tarif zulässt, und wie viele die Anwendung unter Last öffnen kann.
Die letzte Frage beantwortet am Ende ein Lasttest, den der Agent für dich schreiben und gegen eine Testumgebung laufen lassen kann. Gegen die Produktion gehört er nicht.
Backup zurückspielen, von Hand
Das hier erledigst du selbst. Der Grund ist einfach: Im Ernstfall musst du es können, und zwar unter Druck. Im SRE-Buch von Google steht dazu der Satz, der das ganze Thema zusammenfasst: "you only know that you can recover your recent state if you actually do so".
Zuerst klärst du, was es überhaupt gibt. Bei einer gemieteten Datenbank stehen in der Verwaltungsoberfläche des Anbieters drei Angaben: ob Backups aktiv sind, wie lange sie aufbewahrt werden und ob sich ein Stand zu einem beliebigen Zeitpunkt wiederherstellen lässt. Letzteres heißt Point-in-Time Recovery und ist bei manchen Anbietern vom Tarif abhängig. Betreibst du PostgreSQL selbst, beschreibt die Dokumentation beide Wege, den einfachen Dump und die fortlaufende Archivierung.
Dann spielst du einmal zurück, und zwar in eine neue, leere Datenbank, nie über die bestehende. Bei einem gemieteten Dienst ist das meist ein Knopf, der aus einem Backup eine zweite Datenbank erzeugt. Mit den Bordmitteln von PostgreSQL sieht es so aus:
# Sicherung der echten Datenbank ziehen (liest nur)
pg_dump -Fc -d "$DATENBANK_URL" -f sicherung.dump
# In eine neue, leere Datenbank zurückspielen
createdb -T template0 restore_probe
pg_restore --no-owner -d restore_probe sicherung.dump
# Stichprobe: Stimmen die Zahlen?
psql -d restore_probe -c "SELECT count(*) FROM bestellungen;"
Der Tabellenname ist ein Beispiel. Nimm die zwei oder drei Tabellen, auf die es ankommt, und vergleiche die Zeilenzahl und den jüngsten Eintrag mit dem Original. Danach startest du die Anwendung einmal gegen die zurückgespielte Kopie und siehst nach, ob sie läuft.
Drei Dinge schreibst du dabei auf: wie lange es gedauert hat, wie alt der jüngste Datensatz in der Kopie war und welche Zugänge du gebraucht hast. Die ersten beiden Zahlen sagen dir, wie lange die Lösung im Ernstfall steht und wie viele Daten verloren wären. Ob das reicht, ist keine technische Frage. Das entscheidet, wer für das Produkt verantwortlich ist.
Die Sicherungsdatei enthält alle echten Daten. Lösch sie und die Probedatenbank nach dem Test, und leg sie in der Zwischenzeit nicht in einen Ordner, den dein Agent liest.
Der Agent und die echten Daten
Auch das richtest du selbst ein. Die Regel dahinter ist das Prinzip der geringsten Rechte, das OWASP unter dem häufigsten Sicherheitsrisiko für Webanwendungen führt: Zugriff bekommt nur, wer ihn für die Aufgabe braucht, alles andere ist verweigert.
Für einen Coding-Agenten heißt das: Im Alltag arbeitet er mit einer lokalen Datenbank oder einer Testumgebung und kennt die Zugangsdaten zur Produktion gar nicht. Sie stehen also nicht in der .env-Datei auf deinem Rechner, die er lesen kann. Änderungen am Schema erreichen die Produktion über Migrationen in der Auslieferung.
Manchmal muss der Agent trotzdem echte Daten sehen, etwa um einen Fehler zu finden, der nur dort auftritt. Dafür legst du einen eigenen Zugang an, der nur lesen darf. In PostgreSQL gibt es dafür seit Version 14 eine fertige Rolle:
CREATE ROLE agent_lesend LOGIN PASSWORD 'hier-ein-langes-zufallspasswort';
GRANT pg_read_all_data TO agent_lesend;
Die Rolle pg_read_all_data erlaubt laut Dokumentation das Lesen aller Tabellen, als hätte der Zugang überall Leserechte, und sonst nichts. Schreiben, Ändern und Löschen sind nicht dabei. Prüf es, bevor du den Zugang weitergibst: Melde dich damit an und versuch, eine Zeile zu ändern. Die Datenbank muss ablehnen.
Zwei Einschränkungen gehören dazu. Ein lesender Zugang sieht alles, auch personenbezogene Daten. Was der Agent liest, geht bei einem Cloud-Modell an dessen Anbieter. Ob das zulässig ist, klärst du vorher, die Begriffe dazu erklärt DSGVO und KI-Tools. Oft ist eine anonymisierte Kopie der bessere Weg. Und der Zugang gehört nach der Fehlersuche wieder gesperrt.
Was in die Übergabe gehört
Baust du im Auftrag, übergibst du nicht nur Code. Der Kunde braucht die Antwort auf vier Fragen: Wo liegt die Datenbank und wem gehört das Konto? Wo liegen die Backups, und wie lange werden sie aufbewahrt? Wer hat das Zurückspielen zuletzt geprobt, und wie lange hat es gedauert? Welche Zugänge gibt es, und welcher darf was? Die Antworten gehören schriftlich ins Repository. Dein Protokoll vom Restore-Test ist dafür schon der halbe Text.
Bei einem internen Werkzeug stellt dieselben Fragen irgendwann die IT oder der Datenschutz. Es ist angenehmer, sie vorher beantwortet zu haben.
Womit du heute anfängst
Mit dem Restore-Test. Er braucht keinen Agenten und keine Vorarbeit und zeigt dir in einer Stunde, wie es um den wichtigsten Punkt steht. Danach kommt die Regel für Migrationen in die Anweisungsdatei, weil sie ab sofort jeden weiteren Schaden verhindert. Die Prüfungen für Schema, Geldbewegungen und Webhooks gibst du dem Agenten in dieser Reihenfolge, jeweils mit dem Auftrag, erst zu berichten und dann zu ändern.
Quellen15
- Martin Fowler und Pramod Sadalage: Evolutionary Database Designmartinfowler.com
- PostgreSQL-Dokumentation: Indexes, Introductionpostgresql.org
- PostgreSQL-Dokumentation: Using EXPLAINpostgresql.org
- PostgreSQL-Dokumentation: JSON Typespostgresql.org
- PostgreSQL-Dokumentation: Transactionspostgresql.org
- PostgreSQL-Dokumentation: Transaction Isolationpostgresql.org
- PostgreSQL-Dokumentation: Explicit Lockingpostgresql.org
- Stripe-Dokumentation: Webhooksdocs.stripe.com
- PostgreSQL-Wiki: Number Of Database Connectionswiki.postgresql.org
- AWS Lambda: Using Lambda with Amazon RDSdocs.aws.amazon.com
- PostgreSQL-Dokumentation: SQL Dumppostgresql.org
- PostgreSQL-Dokumentation: Continuous Archiving and Point-in-Time Recoverypostgresql.org
- Google SRE Book: Data Integritysre.google
- PostgreSQL-Dokumentation: Predefined Rolespostgresql.org
- OWASP Top 10:2025, A01 Broken Access Controlowasp.org