Workbench: ein Delivery-Index, der falsch liegen darf
Wir haben eine mandantenfähige Sicht über mehrere GitLab-Instanzen gebaut, ohne zur zweiten Wahrheit zu werden. Die Regel, die das trägt: die lokale Datenbank ist wegwerfbar, und korrekt macht sie die Reconciliation, nicht die Webhooks.

Workbench beantwortet eine Frage, die trivial klingt, bis man mehr als eine GitLab-Instanz betreibt: was ist gerade tatsächlich in Arbeit? Work Items, Merge Requests und Milestones liegen über mehrere Instanzen und mehrere Kunden verteilt. Niemand will sechs Tabs öffnen, um zu sehen, ob ein Release blockiert ist.
Der naheliegende Bau ist ein Dashboard, das bei jedem Seitenaufruf alle Instanzen abfragt. Diese Version funktioniert eine Woche lang und stirbt dann: eine langsame Instanz genügt, und die ganze Seite ist langsam. Also haben wir getan, was alle tun: ein PostgreSQL-Lesemodell davor gesetzt.
Genau dort lag die interessante Entscheidung, und sie war keine technische. Sie war eine Regel darüber, was die Datenbank überhaupt wissen darf.
GitLab ist massgebend. Der Index nicht.
Jede Projektionstabelle in Workbench speichert Identifikatoren, Titel, URLs, Zustände, Daten, Personen, Labels und Aktualitätsmetadaten. Sie speichert keine Beschreibungen, keine Kommentare, keinen Code, keine Diffs und keine vollständigen Webhook-Payloads.
Das ist eine harte Regel, keine Empfehlung, und sie wird im Review durchgesetzt. Der Grund sind nicht die Speicherkosten, sondern dass jemand den Inhalt liest, sobald der Index ihn hält, und dass man am Tag der ersten Abweichung eine Lüge mit Zeitstempel ausgeliefert hat. Titel und Zustände driften natürlich auch, aber sie driften sichtbar: ein veralteter Zustand sieht veraltet aus. Ein veralteter Absatz Prosa nicht.
Die praktische Folge: die ganze Datenbank ist wegwerfbar. Wir können sämtliche Projektionstabellen löschen und aus GitLab neu aufbauen. Das ist keine Disaster-Recovery-Geschichte, die wir aufgeschrieben und nie getestet haben; es ist der Normalfall, denn genauso wird ein neuer Kunde aufgeschaltet und genauso wird ein Fehler in der Synchronisation behoben.
Reconciliation ist der Mechanismus. Webhooks sind eine Optimierung.
Alle greifen zuerst zu Webhooks. Signierte Project Hooks treffen in Millisekunden ein und fühlen sich nach der richtigen Antwort an.
Sie sind es nicht, aus einem langweiligen Grund: Webhooks sind eine Zustellgarantie, über die man nicht verfügt. Sie gehen verloren, werden erneut zugestellt, kommen in falscher Reihenfolge an, hören still auf, sobald jemand ein Token rotiert, und für das Zeitfenster zwischen «Projekt in den Scope aufgenommen» und «Hook installiert» existieren sie gar nicht.
Workbench behandelt Hooks deshalb als Beschleuniger über einem System, das auch ohne sie korrekt ist. Ein vollständiger Reconciliation-Lauf liest GitLab und schreibt die Projektion neu; wiederholte Syncs sind idempotent; wird ein Projekt aus dem Scope entfernt, verschwindet es sofort und wird später gelöscht. Würde heute Nacht kein einziger Webhook mehr ankommen, wäre der Index träger, aber korrekt.
Reconciliation ist der Korrektheitsmechanismus. Signierte Project Hooks sind nur ein Beschleuniger.
Diesen Satz vor dem Code in ein ADR zu schreiben, hat den Code verändert. Jedes Mal, wenn wir versucht waren, einen Zustandsübergang nur im Webhook-Pfad zu behandeln, sagte die Regel nein: er muss auch aus einem vollständigen Lesevorgang ableitbar sein.
Schreibvorgänge gehen zuerst nach GitLab und können nicht atomar sein
Lesen ist die einfache Hälfte. Workbench erlaubt auch Änderungen: ein Work Item umbenennen, einen Milestone verschieben, einen Merge Request mergen. Die Reihenfolge liegt fest:
- Lokal autorisieren.
- Nach GitLab schreiben.
- Die Projektion aus der erfolgreichen GitLab-Antwort aktualisieren, nicht aus dem, was wir gesendet haben.
- Auditieren, danach eine gezielte Aktualisierung einreihen.
Schritt drei wiegt schwerer, als er aussieht. Aktualisiert man die Projektion aus dem Request-Body, spiegelt der Index die eigene Absicht; aktualisiert man aus der Antwort, spiegelt er die Realität: inklusive allem, was GitLab unterwegs normalisiert, abgelehnt oder ergänzt hat.
Wir haben diese Einschränkung ins ADR geschrieben statt in ein Ticket, das niemand liest. Das ist der Unterschied zwischen einem bekannten Kompromiss und einem Fehler, den irgendwann ein Kunde findet.
Mandantentrennung: erzwungenes RLS und ein Kontext, den man nicht vergessen kann
Ein Workspace ist ein Kunde, und er ist die Sicherheitsgrenze. Jede Mandantentabelle trägt eine workspace_id ohne NULL, mandantenbewusste Eindeutigkeit und erzwungenes PostgreSQL Row Level Security: erzwungen, damit auch der Tabelleneigentümer es nicht still umgeht.
Anwendungscode filtert nie von Hand nach Workspace. Er betritt einen Kontext:
WorkspaceContext.with(workspace_id) do
MergeRequestProjection.where(state: 'opened').count
endWorkspaceContext.with öffnet eine Transaktion und setzt SET LOCAL app.workspace_id. Die RLS-Policies lesen diese Einstellung. Ausserhalb eines Kontexts existiert sie nicht, und jede Mandantenabfrage schlägt fehl: mit einer Exception, nicht mit einem leeren Resultat. Hintergrundjobs erhalten die Workspace-ID explizit als Argument und stellen ihren eigenen Kontext her; ein Job darf keinen erben.
Der Fehlerfall, gegen den wir konstruiert haben, ist nicht «jemand schreibt eine Abfrage ohne WHERE». Er ist subtiler: eine wiederverwendete Pool-Verbindung, die eine Einstellung in den nächsten Request trägt, oder ein Savepoint-Rollback, das einen Kontext wiederherstellt, den man verlassen glaubte. Genau diese Fälle prüft die Testsuite, neben dem offensichtlichen Lesen und Schreiben über Workspace-Grenzen hinweg.
Drei Tabellengruppen liegen bewusst ausserhalb von RLS, weil sie den Kontext überhaupt erst herstellen: Benutzer, Workspaces, Mitgliedschaften, Einladungen, freigegebene GitLab-Instanzen und GitLab-Identitäten. Plattform-Administratoren müssen weiterhin einen einzelnen Workspace wählen, um an gewöhnliche Mandantendaten zu kommen, das UI hält diese Wahl sichtbar, und der Eintritt wird auditiert.
Zwei Verbindungsmodi, und kein Fallback dazwischen
Ein Workspace erreicht GitLab entweder über die delegierte OAuth-Identität der angemeldeten Person oder über ein Workspace-Service-Token. Welcher Modus greift, ist pro Verbindung explizit.
Ausdrücklich verboten ist der Fallback vom einen auf den anderen. Es sind verlockende zwei Zeilen Code: ist das Token der Person abgelaufen, nimm das Service-Token, und sie verwandeln «diese Person darf dieses Projekt nicht sehen» still in «dieses Projekt ist für alle im Workspace sichtbar». Autorisierung an der GitLab-Grenze bedeutet nur dann etwas, wenn die anfragende Identität die ist, für die man sie hält.
Was ich behalten würde
Drei Dinge aus diesem Bau würde ich in jedes System mitnehmen, das so geformt ist:
- Die Einschränkung vor dem Code aufschreiben. «Reconciliation ist der Korrektheitsmechanismus» ist ein Satz in einem ADR, der ein Dutzend Dateien geprägt hat.
- Die Lücke benennen, die man nicht geschlossen hat. Das Audit-Loch bei GitLab-First-Writes und die KMS-Tabelle ausserhalb von RLS stehen beide im Protokoll. Keines ist eine Überraschung im Wartezustand.
- Den wegwerfbaren Pfad zum Normalfall machen. Ein Neuaufbau, den man wöchentlich fährt, ist ein Neuaufbau, der funktioniert.
Observability, früher in dieser Serie, beginnt bei derselben Form und tut dann eine Sache, die Workbench nie tut: sie wirkt auf die Welt ein. Dieses eine Verb verändert, was «wegwerfbar» wert ist.
Architecture & Governance Lead
Squibble GmbH
Bringt seit über zwanzig Jahren Struktur in gewachsene IT-Landschaften — als Architekt, Entwickler und Betreiber. Schreibt hier über die Systeme, die bei Squibble tatsächlich laufen, und über die Entscheide dahinter.

