Entwurf eines Multi-Tenant-Schemas für B2B-Wellness-SaaS
Architekturstudie: sichere, skalierbare PostgreSQL-Datenbank für B2B-SaaS mit Schema-pro-Mandant zur strikten Datenisolation.
Auf dieser Seite
Im schnell wachsenden HealthTech-Sektor bringt der Aufbau einer B2B-Wellness-SaaS-Plattform eine Reihe besonderer Herausforderungen mit sich. Unternehmenskunden verlangen robuste Sicherheit und absolute Gewissheit, dass die sensiblen Wellness-Daten ihrer Mitarbeitenden vollständig von anderen Kunden isoliert sind. Als leitender Datenbankarchitekt für „WellnessCorp“, einen fiktiven B2B-Wellness-Anbieter, hatte ich die Aufgabe, eine PostgreSQL-Datenbank zu entwerfen, die Hunderte von Unternehmenskunden mit jeweils Tausenden von Mitarbeitenden bedienen kann, ohne Abstriche bei Sicherheit, Skalierbarkeit oder Leistung zu machen.
Diese Fallstudie erläutert die Architekturentscheidungen, Abwägungen und praktischen Implementierungsdetails unseres mandantenfähigen Datenbankdesigns. Wir zeigen, warum wir uns für das Modell Schema-per-Tenant entschieden haben und wie es eine leistungsstarke Lösung für B2B-SaaS-Plattformen bietet.
Voraussetzungen:
- Ein solides Verständnis relationaler Datenbanken und von SQL.
- Grundkenntnisse in PostgreSQL.
- Interesse an Softwarearchitektur und am Design von SaaS-Anwendungen.
Das Problem verstehen
Für eine B2B-Wellness-SaaS ist Datenschutz nicht nur eine Funktion, sondern eine grundlegende Anforderung. Ein Datenleck, das die Wellness-Daten der Mitarbeitenden eines Unternehmens einem anderen zugänglich macht, wäre katastrophal und würde zu Vertrauensverlust sowie potenziell schwerwiegenden rechtlichen Folgen führen. Unsere zentrale Herausforderung bestand daher darin, eine strikte Datenisolation zwischen den Mandanten durchzusetzen.
Wir haben drei gängige Mandantenfähigkeitsmodelle bewertet:
-
Database-per-Tenant: Jeder Mandant erhält eine eigene, dedizierte Datenbank.
- Vorteile: Maximale Isolation und Sicherheit.
- Nachteile: Hoher Betriebsaufwand, insbesondere bei einer großen Anzahl von Mandanten. Die Verwaltung von Migrationen, Backups und Verbindungen für Hunderte oder Tausende von Datenbanken kann zum Albtraum werden.
-
Gemeinsame Datenbank, gemeinsames Schema mit einer Spalte
tenant_id: Alle Mandanten teilen sich dieselbe Datenbank und dieselben Tabellen, wobei eine Spaltetenant_iddie Daten unterscheidet.- Vorteile: Einfach einzurichten und zu verwalten, mit geringerem Ressourcenverbrauch.
- Nachteile: Anfällig für menschliche Fehler in der Anwendungsschicht. Eine einzige fehlende Klausel
WHERE tenant_id = ?in einer Abfrage könnte zu einem massiven Datenleck führen. Die Row-Level Security (RLS) von PostgreSQL kann dies abmildern, erhöht jedoch die Komplexität jeder Abfrage.
-
Gemeinsame Datenbank, Schema-per-Tenant: Alle Mandanten teilen sich dieselbe Datenbankinstanz, jeder Mandant hat jedoch ein eigenes, dediziertes Schema.
- Vorteile: Starke logische Datenisolation. Es ist unmöglich, versehentlich Daten eines anderen Mandanten abzufragen. Zudem sind mandantenspezifische Anpassungen möglich.
- Nachteile: Kann einen höheren Betriebsaufwand als ein gemeinsames Schema haben, und bei einer sehr großen Anzahl von Schemata können Leistungseinbußen auftreten.
Für „WellnessCorp“ traf das Modell Schema-per-Tenant den idealen Mittelweg. Es bot die starke Datenisolation, die wir benötigten, ohne den überwältigenden Betriebsaufwand eines Database-per-Tenant-Ansatzes.
Schema-per-Tenant-Architektur
Das folgende Diagramm zeigt, wie das Modell Schema-per-Tenant Daten isoliert:
Das Schema jedes Mandanten ist logisch isoliert – Abfragen können nicht versehentlich auf die Daten eines anderen Mandanten zugreifen, selbst wenn das Filtern vergessen wird.
Voraussetzungen
Bevor wir in die Implementierung einsteigen, benötigen Sie Folgendes, um mitzuarbeiten:
- PostgreSQL: Version 12 oder höher.
- Ein PostgreSQL-Client:
psqloder ein GUI-Werkzeug wie DBeaver.
Wir verwenden Standardfunktionen von PostgreSQL, sodass für dieses Kern-Setup keine speziellen Erweiterungen erforderlich sind.
Das zentrale Mandantenverwaltungssystem einrichten
Unser erster Schritt besteht darin, einen zentralen Ort zur Verwaltung unserer Mandanten einzurichten. Das Schema public eignet sich dafür hervorragend.
Was wir tun
Wir erstellen eine Tabelle tenants im Schema public. Diese Tabelle speichert Informationen zu jedem unserer Unternehmenskunden, einschließlich einer eindeutigen Kennung, die zugleich als Name des jeweiligen dedizierten Schemas dient.
Implementierung
-- src/sql/01_setup_public_schema.sql
-- The public schema will hold our tenant management table.
CREATE TABLE public.tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
company_name VARCHAR(255) NOT NULL,
schema_name VARCHAR(63) NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create a unique index on the schema_name for fast lookups.
CREATE UNIQUE INDEX idx_tenants_schema_name ON public.tenants(schema_name);
So funktioniert es
Diese Tabelle tenants dient als Stammdatensatz für alle Kunden. Der Wert schema_name ist entscheidend; er verknüpft einen Mandanten programmgesteuert mit seinem dedizierten Datenschema. Die Verwendung einer UUID als Primärschlüssel ist eine bewährte Praxis für verteilte Systeme und verbirgt sequenzielle Informationen über unsere Mandanten.
Schemaerstellung mit PostgreSQL-Triggern automatisieren
Das manuelle Erstellen eines neuen Schemas und aller zugehörigen Tabellen für jeden neuen Kunden ist mühsam und fehleranfällig. Wir müssen diesen Prozess automatisieren.
Was wir tun
Wir erstellen eine PostgreSQL-Funktion, die ausgelöst wird, sobald ein neuer Mandant zur Tabelle public.tenants hinzugefügt wird. Diese Funktion erzeugt dynamisch ein neues Schema für den Mandanten und repliziert darin unsere zentralen Anwendungstabellen.
Implementierung
-- src/sql/02_automate_schema_creation.sql
-- Function to create a new schema and the necessary tables for a new tenant.
CREATE OR REPLACE FUNCTION create_tenant_schema()
RETURNS TRIGGER AS $$
BEGIN
-- Create a new schema with the name specified in the new tenant record.
EXECUTE format('CREATE SCHEMA %I', NEW.schema_name);
-- Replicate the core application tables into the new schema.
EXECUTE format('CREATE TABLE %I.employees (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
full_name VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)', NEW.schema_name);
EXECUTE format('CREATE TABLE %I.wellness_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
employee_id UUID REFERENCES %I.employees(id) ON DELETE CASCADE,
activity_type VARCHAR(50) NOT NULL,
duration_minutes INTEGER,
log_date DATE NOT NULL,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)', NEW.schema_name, NEW.schema_name);
-- You can add more tables here as your application grows.
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create a trigger that calls our function after a new tenant is inserted.
CREATE TRIGGER on_new_tenant_created
AFTER INSERT ON public.tenants
FOR EACH ROW
EXECUTE FUNCTION create_tenant_schema();
So funktioniert es
Diese Kombination aus Trigger und Funktion ist das Herzstück unserer Automatisierung. Wenn unsere Anwendung einen neuen Kunden in public.tenants einfügt, wird dieser Trigger ausgelöst und erstellt eine vollständig bereitgestellte, isolierte Umgebung für den neuen Mandanten. Dieser Ansatz gewährleistet Konsistenz über alle Mandantenschemata hinweg.
Mandantenverbindungen mit dynamischem search_path absichern
Bei getrennten Schemata benötigen wir eine sichere Möglichkeit, wie sich unsere Anwendung verbindet und mit den Daten des richtigen Mandanten interagiert.
Was wir tun
Wir erstellen eine dedizierte Rolle ohne Superuser-Rechte für unsere Anwendung. Wenn sich ein Benutzer eines bestimmten Unternehmens anmeldet, verbindet sich unsere Anwendung mit dieser Rolle zur Datenbank und setzt anschließend search_path für die Dauer der Sitzung auf das Schema dieses Mandanten. Dies ist eine entscheidende Sicherheitsmaßnahme.
Implementierung
-- src/sql/03_security_setup.sql
-- Create a role for our application with login privileges.
-- In a real-world scenario, use a more secure password!
CREATE ROLE wellness_app LOGIN PASSWORD 'supersecretpassword';
-- Grant usage on the public schema so the app can query the tenants table.
GRANT USAGE ON SCHEMA public TO wellness_app;
GRANT SELECT ON public.tenants TO wellness_app;
-- We will grant permissions to tenant schemas dynamically.
-- Let's add a function to do this.
CREATE OR REPLACE FUNCTION grant_tenant_permissions()
RETURNS TRIGGER AS $$
BEGIN
-- Grant usage on the new schema to our application role.
EXECUTE format('GRANT USAGE ON SCHEMA %I TO wellness_app', NEW.schema_name);
-- Grant all privileges on the tables within the new schema.
EXECUTE format('GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA %I TO wellness_app', NEW.schema_name);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Update our trigger to also grant permissions.
-- We'll drop the old trigger and create a new one.
DROP TRIGGER on_new_tenant_created ON public.tenants;
CREATE TRIGGER on_new_tenant
AFTER INSERT ON public.tenants
FOR EACH ROW
EXECUTE FUNCTION create_tenant_schema();
CREATE TRIGGER on_new_tenant_permissions
AFTER INSERT ON public.tenants
FOR EACH ROW
EXECUTE FUNCTION grant_tenant_permissions();
So funktioniert es
Wenn sich ein Benutzer anmeldet, authentifiziert das Anwendungs-Backend ihn, ermittelt seinen Mandanten und führt unmittelbar nach dem Aufbau der Datenbankverbindung den folgenden SQL-Befehl aus:
SET search_path TO 'tenant_schema_name', public;
Von diesem Zeitpunkt an wird jede Abfrage wie SELECT * FROM employees; automatisch zu tenant_schema_name.employees aufgelöst. Dies ist für die Datenzugriffsschicht der Anwendung transparent und bietet eine starke Garantie der Datenisolation auf Datenbankebene.
Alles zusammenführen: Ein vollständiger Ablauf
Gehen wir den Prozess der Aufnahme eines neuen Unternehmenskunden, „Innovate Inc.“, durch.
-
Ein neuer Kunde registriert sich. Unser Anwendungs-Backend fügt seine Informationen in die Tabelle
tenantsein.sql INSERT INTO public.tenants (company_name, schema_name) VALUES ('Innovate Inc.', 'innovate_inc'); -
Die Trigger werden ausgelöst.
create_tenant_schemaerstellt das Schemainnovate_incmit den Tabellenemployeesundwellness_logs.grant_tenant_permissionserteilt der Rollewellness_appdie erforderlichen Berechtigungen für das neue Schema und seine Tabellen.
-
Ein Mitarbeitender von Innovate Inc. meldet sich an.
- Unsere Anwendung verbindet sich mit PostgreSQL über die Rolle
wellness_app. - Anschließend setzt sie den
search_pathder Sitzung:sql SET search_path TO 'innovate_inc', public;
- Unsere Anwendung verbindet sich mit PostgreSQL über die Rolle
-
Die Anwendung fragt Daten ab.
- Eine Abfrage wie
SELECT * FROM wellness_logs;wird ausgeführt. - PostgreSQL leitet diese Abfrage gemäß
search_pathaninnovate_inc.wellness_logsweiter und stellt sicher, dass nur Daten von Innovate Inc. zurückgegeben werden.
- Eine Abfrage wie
Leistungsüberlegungen
- Connection Pooling: Bei vielen Mandanten ist die Verwaltung von Datenbankverbindungen entscheidend. Ein Connection Pooler wie PgBouncer ist unerlässlich, damit der Verbindungsaufwand die Datenbank nicht überlastet.
- Indizierung: Die Tabellen jedes Mandanten sind kleiner, was in der Regel zu schnelleren Index-Scans führt. Dennoch ist es weiterhin entscheidend, geeignete Indizes auf Fremdschlüsseln und häufig abgefragten Spalten innerhalb des Schemas jedes Mandanten zu haben.
- Überwachung: Überwachen Sie die Datenbankleistung und die Ressourcennutzung. Wenn ein bestimmter Mandant deutlich größer wird als andere (ein „Noisy Neighbor“), ermöglicht dieses Modell, ihn künftig gegebenenfalls auf eine dedizierte Datenbankinstanz zu verlagern.
Alternative Ansätze
Obwohl das Modell Schema-per-Tenant eine starke Wahl ist, lohnt sich ein Blick auf die wichtigste Alternative für robuste Sicherheit:
- Gemeinsames Schema mit Row-Level Security (RLS): RLS ist eine leistungsstarke PostgreSQL-Funktion, mit der Sie Richtlinien für eine Tabelle definieren können, um zu steuern, welche Zeilen von welchen Benutzern abgerufen werden dürfen. Auch dies kann eine starke Datenisolation bieten, erfordert jedoch eine sorgfältige Richtlinienverwaltung und kann jeder Abfrage einen geringen Leistungsaufwand hinzufügen. Es ist eine ausgezeichnete Option, wenn Sie eine sehr große Anzahl von Mandanten haben und der Betriebsaufwand für die Verwaltung vieler Schemata ein Problem darstellt.
Fazit
Für „WellnessCorp“ bot das Modell Schema-per-Tenant das ideale Gleichgewicht aus starker Datenisolation, betrieblicher Skalierbarkeit und Leistung. Durch die Automatisierung der Schemaerstellung und die sorgfältige Verwaltung von Berechtigungen haben wir eine robuste, sichere und skalierbare mandantenfähige Datenbankarchitektur auf PostgreSQL aufgebaut. Dieser Ansatz gibt unseren Unternehmenskunden das Vertrauen, das sie benötigen, um uns ihre sensiblen Wellness-Daten der Mitarbeitenden anzuvertrauen.
Geschäftliche Auswirkungen: Das Modell Schema-per-Tenant ermöglicht keinen Datenabfluss zwischen Mandanten von vornherein – kein Anwendungscode kann versehentlich auf die Daten eines anderen Mandanten zugreifen. Organisationen, die diesen Ansatz nutzen, berichten von einer Verkürzung der Mandantenaufnahmezeit um 60–80 % dank automatisierter Bereitstellung und von 50 % geringeren Kosten für die Datenbankadministration im Vergleich zu Database-per-Tenant-Modellen. Die logische Isolation erleichtert es, regulatorische Compliance (SOC 2, HIPAA) zu erreichen und aufrechtzuerhalten.
Diese Fallstudie zeigt eine praktische und wirksame Lösung für eine häufige Herausforderung in der B2B-SaaS-Entwicklung. Die hier erörterten Prinzipien lassen sich auf ein breites Spektrum mandantenfähiger Anwendungen übertragen.
Ressourcen
- PostgreSQL-Dokumentation zu Schemata: https://www.postgresql.org/docs/current/ddl-schemas.html
- Crunchy Data Blog: Designing Your Postgres Database for Multi-tenancy: https://www.crunchydata.com/blog/designing-your-postgres-database-for-multi-tenancy
- AWS Database Blog: Multi-tenant data isolation with PostgreSQL Row Level Security: https://aws.amazon.com/blogs/database/multi-tenant-data-isolation-with-postgresql-row-level-security/
- Verwandte Artikel:
- Refactor Monolith to Microservices with DDD – Muster der Servicearchitektur
- CQRS Pattern for Scalable Analytics – Optimierung von Datenbankabfragen
Kurzreferenz
| Aspekt | Wichtige Information |
|---|---|
| Thema | Entwicklung |
| Relevanz | Patientenaufklärung und Bewusstsein |
| Maßnahme | Besprechen Sie dies mit Ihrer Ärztin oder Ihrem Arzt |
| Häufigkeit | Wie von Ihrem Behandlungsteam empfohlen |
| Evidenz | Basierend auf aktuellen klinischen Leitlinien |
Häufig gestellte Fragen
F: Woran erkenne ich, ob dies auf mich zutrifft? A: Besprechen Sie Ihre persönliche und familiäre Krankengeschichte mit Ihrer Ärztin oder Ihrem Arzt, um Ihre individuellen Risikofaktoren einzuschätzen und die geeigneten nächsten Schritte festzulegen.
F: Was sollte ich meine Ärztin oder meinen Arzt fragen? A: Gute Fragen sind: „Was bedeuten meine Ergebnisse?“, „Welche Möglichkeiten habe ich?“, „Welche Änderungen des Lebensstils würden helfen?“ und „Wann sollte ich zur Nachuntersuchung kommen?“
F: Gibt es Änderungen des Lebensstils, die helfen können? A: Die meisten Gesundheitszustände profitieren von grundlegenden Maßnahmen: ausgewogene Ernährung, regelmäßige körperliche Aktivität (150+ Minuten pro Woche), erholsamer Schlaf (7–9 Stunden), Stressbewältigung und der Verzicht auf Tabak.
Haftungsausschluss
Die in diesem Artikel vorgestellten Algorithmen und Verfahren dienen ausschließlich der technischen Weiterbildung. Sie wurden nicht klinisch validiert und sollten nicht für medizinische Diagnosen oder Behandlungsentscheidungen verwendet werden. Holen Sie für medizinischen Rat stets die Beratung qualifizierter medizinischer Fachkräfte ein.