Multi Tenancy in Rails: Die Entscheidungen die teuer rückgängig zu machen sind

Es gibt eine Kategorie von Entscheidungen in Software die sich im Moment nicht wie Entscheidungen anfühlen. Du bist drei Wochen in den Aufbau eines SaaS Produkts, du brauchst Kunden die sich anmelden, und irgendwo in der ersten Migration fügst du einer Tabelle eine Spalte namens company_id hinzu. Niemand diskutiert das. Es ist offensichtlich. Und zehn Jahre später sitzen vierzig Leute in einem Raum und versuchen herauszufinden wie sie einen Enterprise Kunden auf eine eigene Datenbank verschieben, und die Antwort ist dass es nicht geht, nicht ohne das halbe Produkt neu zu schreiben, wegen einer Spalte die an einem Dienstagnachmittag in Woche drei hinzugefügt wurde. Ich habe in diesem Raum gesessen. Ich war auch schon die Person die die Spalte hinzugefügt hat. Dieser Artikel handelt von der Handvoll Entscheidungen in einer Multi Tenant Rails Anwendung die billig zu treffen und brutal teuer rückgängig zu machen sind, und davon wie du sie bewusst triffst.

Die Spalte die an einem Dienstag hinzugefügt wurde

Es gibt eine Kategorie von Entscheidungen in Software die sich im Moment nicht wie Entscheidungen anfühlen. Du bist drei Wochen in den Aufbau eines SaaS Produkts, du brauchst Kunden die sich anmelden, und irgendwo in der ersten Migration fügst du einer Tabelle eine Spalte namens company_id hinzu. Niemand diskutiert das. Es ist offensichtlich.

Und zehn Jahre später sitzen vierzig Leute in einem Raum und versuchen herauszufinden wie sie einen Enterprise Kunden auf eine eigene Datenbank verschieben, und die Antwort ist dass es nicht geht, nicht ohne das halbe Produkt neu zu schreiben, wegen einer Spalte die an einem Dienstagnachmittag in Woche drei hinzugefügt wurde.

Ich habe in diesem Raum gesessen. Ich war auch schon die Person die die Spalte hinzugefügt hat. Dieser Artikel handelt von der Handvoll Entscheidungen in einer Multi Tenant Rails Anwendung die billig zu treffen und brutal teuer rückgängig zu machen sind, und davon wie du sie bewusst triffst.

Ich habe ihn genauso für Gründer geschrieben wie für Entwickler. Wenn du ein SaaS betreibst oder gerade eines bauen willst, musst du Postgres Schemas nicht verstehen um zu verstehen was dich jede Wahl im vierten Jahr kostet. Auf dieser Ebene bewege ich mich. Wo ich technisch werde, sage ich zuerst warum es fürs Geschäft zählt.

Was ein Tenant eigentlich ist

Ich fange mit dem Wort an, denn die Hälfte der Verwirrung die ich sehe kommt daher dass Leute es locker verwenden.

Ein Tenant ist die Einheit der Datenisolation. Es ist die Grenze innerhalb derer Daten gesehen werden dürfen und außerhalb derer sie es nicht dürfen. Wenn sich ein Kunde in dein Produkt einloggt, sieht er die Daten seines Tenants und sonst niemandes. Das ist die ganze Idee.

Was ein Tenant nicht ist, ist dasselbe wie ein Kunde, ein User, eine Firma oder ein Abrechnungskonto, und in dem Moment in dem du das vermischst hast du eine Entscheidung getroffen die du bereuen wirst.

Nimm ein paar echte Fälle. Eine Agentur meldet sich bei deinem Produkt an und betreut damit zwölf Kunden. Ist die Agentur der Tenant, oder ist es jeder Kunde? Wenn die Agentur der Tenant ist, liegen die Daten der Kunden in einem Topf und die Mitarbeiter der Agentur sehen alles. Wenn jeder Kunde ein Tenant ist, müssen die Mitarbeiter der Agentur zu zwölf Tenants gehören und zwischen ihnen wechseln. Beides ist legitim. Es sind verschiedene Produkte.

Eine Firma hat eine britische und eine deutsche Gesellschaft die aus rechtlichen Gründen die Daten der jeweils anderen nicht sehen dürfen, aber ein Finanzchef zahlt für beide. Zwei Tenants, ein Abrechnungskonto.

Ein Freelancer meldet sich allein an, stellt dann zwei Leute ein, wird dann übernommen. Ein Tenant der mit einem User beginnt und am Ende einer anderen Firma gehört.

Wenn du nicht entschieden hast was dein Tenant ist, hat dein Code es für dich entschieden, und er hat fast sicher entschieden dass Tenant gleich Firma gleich Abrechnungskonto gleich das Ding mit dem Abo ist. Das funktioniert genau bis zum ersten Kunden dessen Organisation anders geformt ist, und den gibt es immer.

Die erste teure Entscheidung ist also eine Definition. Schreib auf was ein Tenant in deinem Produkt ist. Schreib auf was er nicht ist. Dann sorg dafür dass das Datenmodell genau das abbildet und nichts anderes.

Die drei Arten Tenants auseinanderzuhalten

Es gibt drei Isolationsmodelle und jeder der SaaS baut landet auf einem davon. Ich beschreibe jedes ehrlich, einschließlich dessen was schiefgeht.

Geteiltes Schema mit Tenant Spalte. Eine Datenbank, ein Satz Tabellen, und jede Zeile die zu einem Tenant gehört trägt eine Spalte die sagt zu welchem. Jede Abfrage filtert darauf. Das machen neunzig Prozent aller SaaS Produkte und das würde ich für fast jedes Produkt wählen zu dem ich gefragt werde.

Das Gute: es ist billig. Eine Datenbank zum Sichern, eine zum Überwachen, eine Migration wenn du eine Tabelle änderst. Einen Tenant hinzufügen heißt eine Zeile einfügen. Zehntausend Tenants kosten im Betrieb ungefähr dasselbe wie zehn. Tenant übergreifendes Reporting, die Art die du für deine eigene Analyse brauchst und für alles was Kunden miteinander vergleicht, ist eine Abfrage.

Das Schlechte: die Isolation ist ein Versprechen das dein Code gibt, keine Wand die die Datenbank durchsetzt. Vergiss den Filter einmal und Tenant A sieht Tenant B. Laute Nachbarn sind real: ein Kunde der einen riesigen Export laufen lässt bremst alle. Backup und Wiederherstellung pro Tenant gibt dir die Datenbank nicht, das musst du bauen. Und wenn der Enterprise Kunde verlangt dass seine Daten in Frankfurt liegen während alle anderen in London sind, ist die ehrliche Antwort dass sie das nicht tun.

Schema pro Tenant. Eine Datenbank, aber jeder Tenant bekommt sein eigenes Postgres Schema, einen Namensraum mit einer vollständigen Kopie jeder Tabelle. Die Anwendung schaltet bei jedem Request den Suchpfad auf das Schema des Tenants und alle Abfragen sehen aus wie Code für einen einzelnen Mandanten.

Das Gute: die Isolation ist strukturell. Eine Abfrage im falschen Schema liefert nichts statt der Daten von jemand anderem. Wiederherstellung pro Tenant ist möglich weil du ein Schema dumpen kannst. Entwickler die noch nie Multi Tenant Software gebaut haben finden es intuitiv.

Das Schlechte: Migrationen. Jede Schemaänderung muss einmal pro Tenant laufen. Bei zehn Tenants ist das eine Schleife. Bei tausend ist es ein Deployment das eine Stunde dauert und auf halbem Weg scheitert, und die Hälfte deiner Kunden ist auf dem neuen Schema und die andere Hälfte auf dem alten. Postgres selbst kommt bei zehntausenden Tabellen ins Schwitzen, die Systemkataloge blähen sich auf, Connection Pooling wird kompliziert, und Werkzeuge die ein Schema voraussetzen, also die meisten Werkzeuge, gehen auf interessante Weise kaputt. Ich habe ein Team ein volles Quartal damit verbringen sehen das bei rund achthundert Tenants zurückzubauen. Das Rails Ökosystem hatte ein beliebtes Gem für dieses Modell, und dessen eigene Maintainer haben irgendwann einen langen Beitrag darüber geschrieben warum sie es nicht mehr empfehlen.

Datenbank pro Tenant. Jeder Tenant bekommt eine ganze Datenbank, möglicherweise auf einem eigenen Server.

Das Gute: vollständige Isolation, die Art auf die du in einem Sicherheitsfragebogen zeigen kannst. Datenstandort ist trivial: die Datenbank des deutschen Kunden steht in Deutschland. Wiederherstellung pro Tenant, Skalierung pro Tenant, alles pro Tenant. Ein Kunde kann gehen und seine Datenbank mitnehmen.

Das Schlechte: Kosten und Betrieb. Jede Datenbank braucht Verbindungen, Monitoring, Backups, Upgrades. Deine Infrastrukturrechnung skaliert mit der Kundenzahl statt mit der Nutzung. Migrationen laufen einmal pro Datenbank und können jetzt pro Datenbank scheitern. Alles Tenant übergreifende ist ein Data Warehouse Projekt. Einen Kunden anzulegen heißt Infrastruktur bereitzustellen, also entweder ein Mensch oder eine Menge Automatisierung. Dieses Modell ist richtig für ein Produkt mit fünfzig Kunden die je fünfzigtausend im Jahr zahlen. Es ist ein langsamer Tod für ein Produkt mit fünftausend Kunden die fünfzig im Monat zahlen.

Wie jedes Modell bei zehn, hundert und zehntausend scheitert

Es hilft das danach zu betrachten wo jedes Modell bricht, denn der Bruchpunkt bestimmt die Kosten der Umkehr.

Bei zehn Tenants funktioniert alles. Geteiltes Schema ist in Ordnung. Schema pro Tenant ist in Ordnung und fühlt sich sicherer an. Datenbank pro Tenant ist in Ordnung und fühlt sich nach Enterprise an. Das ist der Punkt an dem die Entscheidung getroffen wird, und es ist der Punkt an dem jede Option gleich gut aussieht, was genau das Problem ist.

Bei hundert Tenants zeigt das Schema pro Tenant Modell seine Migrationskosten, aber es ist noch handhabbar. Datenbank pro Tenant ist jetzt eine spürbare Infrastrukturrechnung und eine echte Betriebslast, und wenn deine Tenants dir dreißig Euro im Monat zahlen verlierst du an jedem Geld. Das geteilte Schema hat wahrscheinlich seinen ersten Schreck mit einem Tenant Leck hinter sich, eine Abfrage die jemand vergessen hat zu scopen, im Code Review entdeckt oder, schlimmer, von einem Kunden.

Bei zehntausend Tenants steht ohne Neubau nur noch das geteilte Schema. Schema pro Tenant ist zur Vollzeitstelle für eine Person geworden und zu einer Quelle von Deployment Angst. Datenbank pro Tenant ist in dieser Größe ein Hosting Unternehmen, kein SaaS.

Jetzt die Kosten der Umkehr. Von einem geteilten Schema auf Datenbank pro Tenant zu wechseln, für einen Kunden, ist möglich wenn du diszipliniert warst, und ich komme darauf zurück wie. Von Schema pro Tenant auf ein geteiltes Schema ist ein Migrationsprojekt das jede Tabelle, jede Abfrage und jedes Deployment Skript berührt. Von Datenbank pro Tenant auf irgendetwas anderes heißt tausende Datenbanken zusammenzuführen und jeden Primärschlüssel neu zu nummerieren. Ich habe dieses Projekt schon angeboten. Niemand hat das Angebot je angenommen.

Deshalb ist das geteilte Schema mein Standard. Nicht weil es am ersten Tag das sicherste ist, das ist es nicht, sondern weil es das einzige ist dessen Fehlerbilder sich an Ort und Stelle beheben lassen statt durch Neuanfang.

Wo der Tenant im Code lebt

Wenn du das geteilte Schema nimmst, und das solltest du, ist die nächste teure Entscheidung wo der aktuelle Tenant lebt und wie Abfragen ihn finden.

Die verlockende Antwort ist ein Default Scope: sag jedem Modell dass es den Tenant Filter automatisch an jede Abfrage hängt. Es fühlt sich sicher an. Es ist das Gegenteil. Default Scopes in Rails sind berüchtigt schwer nachzuvollziehen, sie sickern in Stellen die du nicht gemeint hast, sie machen es unmöglich die Admin Werkzeuge zu schreiben die legitim über Tenants hinweg sehen müssen, und sie geben dir ein falsches Sicherheitsgefühl das dich davon abhält die Tests zu schreiben die ein Leck wirklich fangen würden.

Was ich stattdessen mache, und worauf ich in jedem Aufbau bestehen würde für den ich verantwortlich bin, sind drei Dinge.

Erstens lebt der aktuelle Tenant an genau einer Stelle. Rails hat dafür einen Mechanismus namens Current Attributes: ein Container pro Request der einmal am Rand gesetzt wird, wenn der Request reinkommt und der User identifiziert ist, und am Ende des Requests zurückgesetzt wird. Alles andere fragt diese eine Stelle. Es gibt keinen zweiten Weg herauszufinden in welchem Tenant du bist.

Zweitens wird jede Abfrage die Tenant Daten berührt explizit gescopet, über den Tenant. Du fragst nicht nach allen Rechnungen und filterst. Du fragst den aktuellen Tenant nach seinen Rechnungen. In Rails Begriffen sind das Assoziationen, und das heißt die Tenant ID ist in der Abfrage weil sie strukturell nicht fehlen kann. Code der nicht über den Tenant geht sieht auf der Seite falsch aus, und genau das ist der Punkt: Reviewer sehen es.

Drittens setzt es auch die Datenbank durch. Postgres hat eine Funktion namens Row Level Security mit der du an eine Tabelle eine Richtlinie hängst die sagt dass Zeilen nur sichtbar sind wenn eine Session Variable zur Tenant Spalte passt. Die Anwendung setzt die Variable am Anfang jedes Requests. Wenn jemand eine ungescopte Abfrage schreibt, liefert die Datenbank nichts statt alles. Das ist die Gürtel und Hosenträger Schicht. Sie kostet einen Tag Einrichtung und sie hat in Produktion Fehler gefangen die jede andere Schicht übersehen hat. Ich würde ein SaaS mit geteiltem Schema nicht mehr ohne betreiben.

Es gibt Gems die Teile davon verpacken. Das bekannte ist acts_as_tenant und es ist in Ordnung. Meine Sicht ist dass der Mechanismus klein genug ist um ihn selbst zu besitzen, und ihn zu besitzen heißt ihn zu verstehen, was hier mehr zählt als fast überall sonst im Code. Aber wenn dein Team das Gem bevorzugt, nimm das Gem. Die Entscheidung die teuer rückgängig zu machen ist, ist nicht Gem gegen selbst gebaut. Es ist Default Scope gegen explizites Scoping.

Die Stellen an denen Tenants lecken und an die niemand denkt

Das ist die Sache mit dem Request Zyklus: er ist der einfache Teil. Der Tenant kommt mit dem User rein, du setzt ihn, du scopest die Abfragen, fertig. Die Lecks passieren überall dort wo kein Request ist.

Background Jobs. Ein Job wird während eines Requests eingereiht, mit gesetztem Tenant. Er läuft zehn Minuten später in einem Worker Prozess in dem nichts gesetzt ist. Wenn dein Job Code annimmt dass der Tenant da ist, läuft er jetzt ohne Tenant, was mit ordentlicher Row Level Security heißt dass er nichts tut, und ohne heißt dass er alles tut. Jeder Job braucht den Tenant serialisiert in seinen Argumenten und wiederhergestellt wenn er läuft. Das ist das häufigste Tenant Leck das ich in Code Audits gefunden habe. Über die breitere Disziplin habe ich in meinem Artikel über Background Workers geschrieben, und das hier ist der Multi Tenant Nachtrag dazu.

Caches. Du cachst ein gerendertes Fragment, eine berechnete Summe, eine API Antwort. Der Cache Schlüssel ist die ID des Datensatzes. IDs sind in einem geteilten Schema global über Tenants hinweg, aber ein Fragment das für das Dashboard von Tenant A gecacht wurde wird jetzt an Tenant B ausgeliefert wenn der Schlüssel den Tenant nicht enthält. Jeder Cache Schlüssel braucht den Tenant. Jeder einzelne.

Dateispeicher. Uploads gehen mit einem Pfad in den Objektspeicher. Wenn der Pfad nur die ID der Datei ist, ist eine signierte URL für eine Datei eine signierte URL für jede Datei deren ID du erraten kannst. Stell jedem gespeicherten Objekt den Tenant voran. Das macht Export und Löschung pro Tenant außerdem zu einer Präfix Operation statt zu einem Datenbank Join.

Suchindizes. Wenn du Elasticsearch, Meilisearch oder Postgres Volltext nutzt, braucht der Index den Tenant als Filter der bei jeder Suche angewendet wird, nicht als Feld bei dem man dem Aufrufer vertraut es hinzuzufügen. Die Zahl der SaaS Produkte bei denen der Suchendpunkt das Leck ist, ist nicht klein.

Logs und Fehlerberichte. Weniger ein Leck gegenüber Kunden, mehr gegenüber deinem eigenen Team. Eine Logzeile mit Kundendaten und ohne Tenant ID ist ein Compliance Problem das darauf wartet zu passieren, weil du sie nicht finden und löschen kannst wenn der Kunde geht.

Primärschlüssel. Wenn deine IDs fortlaufende Ganzzahlen sind, verraten sie die Größe deines Geschäfts und lassen einen neugierigen Kunden durchzählen. Wenn deine URLs sie enthalten, landet ein Tippfehler jemanden auf dem Datensatz eines anderen Tenants, und nur dein Scoping steht zwischen ihm und dem Sehen. Nimm UUIDs, oder zeig zumindest keine fortlaufenden IDs in URLs. Das ist streng genommen keine Tenancy Entscheidung, aber eine die später teuer zu ändern ist, denn jede URL in jeder Mail die du je verschickt hast enthält die alte Form.

User die zu mehr als einem Tenant gehören

Der zweithäufigste teure Fehler, nach dem Default Scope, ist die Tenant ID auf die Users Tabelle zu legen.

Es wirkt offensichtlich. Ein User arbeitet für eine Firma, also gehört ein User zu einem Tenant. Außer im Agenturfall von vorhin: deren Mitarbeiter müssen in zwölf sein. Der Berater der drei deiner Kunden betreut und einen Login will. Der Gründer der zwei Geschäfte auf deinem Produkt betreibt. Der Kunde der übernommen wird und jetzt Mitarbeiter der Muttergesellschaft hat die Zugang brauchen. Der Support Engineer in deinem eigenen Team der mit Erlaubnis des Kunden in dessen Konto schauen muss.

Jeden dieser Fälle gibt es in jedem SaaS an dem ich gearbeitet habe, und jedes Produkt das die Tenant ID auf den User gelegt hat, hat am Ende entweder einen hässlichen Umweg gebaut, meist mit mehreren Konten pro Person, oder die Migration gemacht.

Das Modell das überlebt ist: User sind global, Tenants sind global, und dazwischen liegt eine Mitgliedschaftstabelle die sagt welche User zu welchen Tenants mit welcher Rolle gehören. Ein User loggt sich einmal ein, sieht die Tenants zu denen er gehört, wählt einen, und die Session trägt von da an den aktuellen Tenant. Einladungen werden zu einer Zeile in der Mitgliedschaftstabelle die darauf wartet dass ein User sie einlöst. Rollen liegen auf der Mitgliedschaft, nicht auf dem User, weil dieselbe Person in einem Tenant Admin und in einem anderen nur lesend sein kann.

Single Sign On kommt später, und dieses Modell macht es möglich. Wenn der Enterprise Kunde will dass sich seine Mitarbeiter über den eigenen Identity Provider einloggen, hängst du den Identity Provider an den Tenant, und die Mitgliedschaftstabelle weiß schon wer rein darf. Läge die Tenant ID auf dem User, hieße SSO die Authentifizierung neu zu bauen.

Das kostet am ersten Tag ungefähr einen Tag mehr als die naive Version. Es spart später Monate.

Konfiguration, Feature Flags und die Form eines Tenants

Tenants sind nicht identisch. Einer hat den Enterprise Plan, einer ist im Test, einer hat ein Feature das du von Hand freigeschaltet hast weil er nett gefragt hat und ein guter Referenzkunde ist.

Der teure Fehler hier ist Konfiguration in Code zu legen. Eine Bedingung die prüft ob der Tenant der eine spezielle Kunde ist, dann noch eine, dann ein Case Statement, und drei Jahre später gibt es eine Datei die niemand anzufassen wagt und die tenant_overrides heißt.

Die Version die gut altert ist langweilig: ein Settings Modell pro Tenant mit typisierten Werten, und ein Feature Flag System das Flags pro Tenant mit vernünftigen Standardwerten auflöst. Beides kann jemand der kein Entwickler ist über einen Admin Bildschirm bearbeiten. Beides ist an einer Stelle lesbar wenn ein Kunde fragt warum sich sein Konto anders verhält. Für die Flags gibt es gute Gems, Flipper ist das zu dem ich greife.

Die verwandte Entscheidung ist was ein Plan ist. Ein Plan ist kein Attribut des Tenants. Ein Plan ist ein Bündel aus Limits und Flags, und ein Tenant ist auf einem. Wenn du Plannamen fest in den Code schreibst, ist der Tag an dem das Marketing die Pläne umbenennt eine Codeänderung. Wenn Pläne Daten sind und Tenants darauf verweisen, ist es eine Bearbeitung im Admin.

Der Tenant ist nicht das was zahlt

Ich habe am Anfang gesagt dass der Tenant nicht das Abrechnungskonto ist, und ich will ausführen warum das zählt, denn es ist eine Entscheidung die wie ein Buchhaltungsdetail aussieht und sich als strukturell herausstellt.

Ein Abrechnungskonto ist die Einheit die eine Zahlungsmethode hat, Rechnungen erhält und dir Geld schuldet. Ein Tenant ist die Einheit deren Daten isoliert sind. Normalerweise ist das eins zu eins. Nicht immer, und die Fälle in denen es auseinandergeht sind die Kunden die dir am meisten zahlen.

Die Holding die für die Tenants von fünf Tochtergesellschaften auf einer Rechnung zahlt. Die Agentur die für die Tenants ihrer Kunden zahlt und aufschlägt. Das Enterprise das will dass der Tenant existiert bevor der Einkauf fertig ist, mit Abrechnung die sechs Wochen später drankommt. Der Reseller.

Wenn dein Abo Datensatz am Tenant hängt, sind das alles Umwege. Wenn ein Tenant ein Abrechnungskonto hat und ein Abrechnungskonto viele Tenants hat, sind das alles Zeilen. Die Migration vom ersten zum zweiten ist im Code nicht riesig, aber in den Daten ist sie es, denn jede historische Rechnung muss neu zugeordnet werden, und dein Steuerberater hat dazu eine Meinung.

Entscheide es am ersten Tag. Es ist eine zusätzliche Tabelle.

Warum das Isolationsmodell auch deine DSGVO Strategie ist

Ich habe bisher von der Engineering Seite geschrieben, also wechsle ich zur rechtlichen Seite, denn es ist dieselbe Entscheidung aus zwei Blickwinkeln.

Unter der DSGVO, und dasselbe gilt für die britische Fassung, ist jeder deiner Kunden Verantwortlicher und du bist sein Auftragsverarbeiter. Jeder von ihnen kann dich im Namen seiner eigenen Nutzer bitten alles zu exportieren was du über eine Person hast, oder es zu löschen. Jeder von ihnen braucht einen Auftragsverarbeitungsvertrag mit dir der sagt wo seine Daten sind und wer sie sonst noch berührt. Jeder von ihnen hat das Recht zu gehen und seine Daten mitzunehmen.

Jede dieser Pflichten ist leicht oder schwer je nach Isolationsmodell.

Export. In einem geteilten Schema ist der Export eines Tenants eine Abfrage pro Tabelle gefiltert nach Tenant ID plus eine Präfix Auflistung im Dateispeicher. Wenn du diszipliniert warst und die Tenant Spalte überall ist, ist es ein Skript. Wenn nicht, ist es Archäologie.

Löschung. Dasselbe nochmal, und hier beißen die Log und Cache Lecks zurück. Wenn die Daten eines Kunden in einer Logzeile ohne Tenant ID stehen, kannst du nicht beweisen dass du sie gelöscht hast.

Datenstandort. Wenn der Vertrag des deutschen Kunden Frankfurt sagt, erfüllt ein geteiltes Schema in London das nicht, und keine Row Level Security der Welt ändert das. Das ist die eine Pflicht die nur das Datenbank pro Tenant Modell von Haus aus erfüllt, und deshalb gibt es den nächsten Abschnitt.

Verarbeitungsverzeichnis. Dein Verzeichnis muss sagen welche Daten du für wen wo hältst. Eine Tenant Tabelle mit einer Spalte für den Standort und eine klare Liste der Unterauftragsverarbeiter sind dieses Verzeichnis. Ein Haufen Sonderfälle ist es nicht.

Die KI spezifische Version davon habe ich in DSGVO konforme KI Features in deinem SaaS bauen vertieft, und alles in dem Artikel setzt voraus dass du die Tenancy darunter richtig hast. Du kannst Datenschutz nicht an ein Produkt anschrauben das nicht weiß wessen Daten wessen sind.

Der Moment in dem das Enterprise eine eigene Datenbank will

Er wird kommen. Ein Kunde der groß genug ist um zu zählen wird in einem Sicherheitsreview oder einem Einkaufsgespräch sagen dass seine Richtlinie verlangt dass seine Daten in einer dedizierten Datenbank liegen, oder in einem bestimmten Land, oder beides. Du wirst ja sagen wollen.

Ob du billig ja sagen kannst hängt vollständig von Entscheidungen ab die du Jahre vorher getroffen hast. Das hier muss wahr sein.

Die Tenant ID darf nie an Stellen durchgesickert sein an die sie nicht gehört. Nicht in URLs als Routing Schlüssel. Nicht in fest verdrahtete Joins über Tenants hinweg. Nicht in Annahmen im Reporting Code dass alle Tenants in einer Datenbank liegen. Wenn die ganze Anwendung Tenant Daten nur erreicht indem sie den aktuellen Tenant fragt, dann ist es ein Detail das variieren kann in welcher Datenbank der aktuelle Tenant lebt.

Rails unterstützt seit einigen Jahren nativ mehrere Datenbanken, einschließlich des Wechsels der Verbindung pro Request. Das Muster das funktioniert ist: die meisten Tenants leben in der geteilten Datenbank, ein Tenant Datensatz trägt einen optionalen Verweis auf eine dedizierte Datenbank, und der Verbindungswechsel passiert an derselben Stelle an der der aktuelle Tenant gesetzt wird. Alles unterhalb dieser Linie weiß es nicht und kümmert sich nicht.

Das baust du nicht am ersten Tag. Du baust am ersten Tag die Disziplin die es möglich macht, und du baust den Schalter beim ersten Kunden der bereit ist dafür zu zahlen. Ich habe genau diesen Schritt für einen Kunden des Produkts eines Auftraggebers gemacht, und es hat etwa drei Wochen gedauert, weil die Disziplin da war. Ich bin auch schon gebeten worden es bei einem Produkt mit Default Scopes und Ganzzahl IDs in jeder URL zu machen, und das ehrliche Angebot war ein Neubau.

Halte die Tür offen. Es kostet nichts sie offen zu halten und ein Vermögen sie wieder aufzumachen.

Der eine Test den jedes SaaS braucht

Ich bin ein Test First Entwickler und habe in meinem Leitfaden zu TDD und BDD in Rails ausführlich geschrieben warum, also beschränke ich mich hier auf den konkreten Punkt.

Die meisten Testsuiten für Multi Tenant Produkte testen dass Dinge funktionieren. Sehr wenige testen dass Dinge über die Grenze hinweg nicht funktionieren. Und das ist der Test der zählt, denn ein Feature das versagt ist ein Fehlerbericht, während ein Tenant Leck eine Datenpanne mit zweiundsiebzig Stunden Meldefrist ist und ein Eintrag in deinem Vorfallsprotokoll nach dem jeder künftige Enterprise Kunde fragen wird.

Der Test ist einfach zu beschreiben. Lege zwei Tenants an. Lege in beiden Daten an. Logge dich als User von Tenant A ein. Versuche auf jede Art die dir einfällt an die Daten von Tenant B zu kommen: die Listenendpunkte, die Detailendpunkte mit den IDs von Tenant B, die Suche, den Export, die API, den Background Job mit gefälschtem Argument, das gecachte Fragment. Prüfe dass jeder einzelne davon nichts oder ein Nicht gefunden liefert. Dann dasselbe in die andere Richtung.

Schreib ihn als Cucumber Feature wenn das Geschäft ihn lesen können soll, denn das hier ist einer den das Geschäft lesen sollte. Lass ihn bei jedem Commit laufen. Füge jedes Mal einen Fall hinzu wenn du einen Endpunkt hinzufügst. Es ist mühsam. Es ist der wertvollste Test in der Suite.

Und dann gibt es die Version des Tests die gegen die Datenbank läuft statt gegen die Anwendung: mit Row Level Security an Ort und Stelle, verbinde dich als Anwendungsrolle mit gesetzter Session Variable für Tenant A, führe ein rohes Select ohne Filter aus, und prüfe dass du nur die Zeilen von Tenant A bekommst. Dieser Test beweist dass die Datenbank richtig liegt selbst wenn die Anwendung falsch liegt.

Was ich für GrowCentric gewählt habe, und warum

Ich nehme mein eigenes Produkt als durchgerechnetes Beispiel, mit dem Vorbehalt dass ich über die Überlegungen spreche und nicht über die Innereien, denn die Innereien sind nicht der Punkt.

GrowCentric ist eine eCommerce Growth Plattform. Mehrere Kundenshops verbinden sich damit, sie sammelt deren Daten, und eines der Dinge die sie tut ist Benchmarking: so schneidet deine Conversion Rate im Vergleich zu Shops wie deinem ab. Dieses Feature ist nur möglich weil die Plattform über Tenants hinweg schauen kann, aggregiert und anonymisiert, unter einer Lizenz die die Nutzungsbedingungen ausdrücklich einräumen. Es ist aus Datenschutzsicht auch das Sensibelste was das Produkt tut, und deshalb legen die Zugangsbedingungen genau fest was aggregiert wird und warum.

Diese eine Produktanforderung hat die Isolationsentscheidung für mich getroffen. Ein Datenbank pro Tenant Modell hätte aus dem Benchmark ein Warehouse Projekt gemacht und das Kernfeature des Produkts zum Schwierigsten gemacht was es zu bauen gibt. Schema pro Tenant hätte es möglich aber schmerzhaft gemacht. Geteiltes Schema mit Tenant Spalte, striktes explizites Scoping, Row Level Security darunter, und eine sehr klare Linie im Code zwischen "diese Abfrage ist auf einen Tenant gescopet" und "diese Abfrage ist der aggregierte Benchmark und nur von einer Stelle aus erreichbar", war das Modell das zu dem passt was das Produkt wirklich ist.

Ich wusste außerdem von der ersten Woche an dass User zu mehreren Tenants gehören würden, denn eine Agentur die mehrere Shops betreut ist ein Kunde den ich will. Also sind User global mit Mitgliedschaften. Und ich wusste dass EU Kunden nach dem Datenstandort fragen würden, also trägt der Tenant Datensatz wo die Daten liegen, auch solange jeder Tenant am selben Ort ist, denn an dem Tag an dem einer woanders sein muss ändere ich lieber einen Wert als ein Schema.

Nichts davon hat länger gedauert als die naive Version gedauert hätte. Es hat einen Nachmittag Nachdenken vor der ersten Migration gekostet statt nach dem ersten Enterprise Anruf.

Die Liste, für den Gründer in Eile

Wenn du nur einen Abschnitt liest, lies diesen und gib ihn dem der dein Produkt baut.

  1. Definiere schriftlich was ein Tenant in deinem Produkt ist, vor der ersten Migration.
  2. Geteiltes Schema mit Tenant Spalte, außer du hast einen konkreten Grund dagegen, und "es fühlt sich sicherer an" ist kein Grund.
  3. Der aktuelle Tenant lebt an einer Stelle, gesetzt am Rand jedes Requests und jedes Jobs.
  4. Jede Abfrage geht über den Tenant. Keine Default Scopes. Niemals.
  5. Postgres Row Level Security als Schicht unter der Anwendung.
  6. Tenant in jedem Cache Schlüssel, jedem Dateipfad, jedem Suchfilter, jedem Job Argument, jeder Logzeile.
  7. User sind global. Mitgliedschaften verbinden User mit Tenants und tragen die Rolle.
  8. Tenant hat ein Abrechnungskonto. Abrechnungskonto hat viele Tenants.
  9. Konfiguration und Pläne sind Daten mit Admin Bildschirm, keine Bedingungen im Code.
  10. UUIDs, oder zumindest keine fortlaufenden IDs in URLs.
  11. Ein Feld für den Datenstandort am Tenant vom ersten Tag an, auch wenn jeder Wert derselbe ist.
  12. Der Tenant A kann Tenant B nicht sehen Test, bei jedem Commit, wachsend mit jedem Endpunkt.

Jeder dieser Punkte ist am Anfang ein Tag oder weniger. Jeder davon sind Wochen bis Monate wenn du ihn mit Kunden auf der Plattform nachrüsten musst.

Das ehrliche Ende

Ich höre auf wo ich angefangen habe. Die Spalte die an einem Dienstagnachmittag hinzugefügt wurde ist kein Fehler weil die Spalte falsch ist. Eine Tenant Spalte ist genau richtig. Sie ist ein Fehler weil niemand sie entschieden hat, also hat niemand die elf Dinge entschieden die dazugehören, und das Produkt ist um die Lücken herum gewachsen.

Die gute Nachricht ist dass all das billig ist wenn du es am Anfang machst, und ich meine billig: ein Settings Modell, eine Mitgliedschaftstabelle, eine Session Variable, ein Test. Die teure Version ist die in der du die Lücken durch einen Kunden entdeckst. Wenn du am Anfang stehst, investiere den Nachmittag. Wenn du darüber hinaus bist und etwas in diesem Artikel dich hat zusammenzucken lassen, ist es meistens besser zu beheben als es sich anfühlt, und die Reihenfolge ist die Reihenfolge der Liste oben.

Wenn du ein SaaS auf Rails startest, oder du lebst mit einer Tenancy Entscheidung die jemand vor Jahren getroffen hat und die angefangen hat zu beißen, lass uns reden. Ich gehe die Entscheidungen mit dir durch bevor sie teuer werden, und wenn die Antwort ist alles so zu lassen wie es ist, sage ich das.

1%jeder Rechnung geht an eine Charity, die du wählst.

Eine Spende, nie Sponsoring. Du wählst das Anliegen beim Onboarding.

Die Geschichte hinter dem Versprechen →

Bleib deiner Konkurrenz voraus.

Die neuesten innovativen Produkte und Services, direkt in dein Postfach, bevor deine Mitbewerber davon hören.

Sichere dir bis zu 5% auf deine ersten sechs Monate: 1% pro gewähltem Thema, die vollen 5% wenn du alles nimmst. Limitiertes Angebot · endet am 31. Dezember 2026.

Nur für Neukunden. Es gelten die AGB.

* Bis zu 5% auf deine ersten sechs Monatsrechnungen, nur für Neukunden. Alle Bedingungen.

Fragen zu Preisen, Verträgen oder zur Zusammenarbeit?

Zu den FAQ