GraphQL Multi-Tenancy: mandantenfähige Schemas und Resolver bauen
AI generated
{ }
type
GraphQL · Multi-Tenancy · SaaS · Security
GraphQL Multi-Tenancy
mandantenfähige Schemas und Resolver richtig bauen

Eine gemeinsame GraphQL-API für mehrere Mandanten spart Infrastruktur, birgt aber ein reales Risiko: ein einziger vergessener Tenant-Filter im Resolver, und Mandant A sieht Daten von Mandant B. Wer Tenant-Isolation systematisch statt ad hoc aufbaut, vermeidet genau dieses Szenario.

20 Min. Lesezeit Tenant-Kontext · Row-Level-Security · Schema Stitching Node.js · SaaS-Architektur

1. Warum Multi-Tenancy in GraphQL andere Fragen aufwirft als in REST

Bei REST-APIs lässt sich Multi-Tenancy oft schon auf URL-Ebene lösen, etwa über /tenants/{id}/orders, und jeder Endpoint bekommt seine eigene, isolierte Tenant-Prüfung. GraphQL kennt diese Struktur nicht: Ein einziger Endpoint beantwortet beliebig verschachtelte Queries, die in einer einzigen Anfrage durch mehrere Typen und Resolver wandern. GraphQL Multi-Tenancy muss die Tenant-Isolation deshalb nicht an einem einzelnen Einstiegspunkt, sondern konsequent auf jeder einzelnen Resolver-Ebene durchsetzen.

Diese Eigenschaft macht GraphQL einerseits anfälliger für Isolationsfehler, weil ein einziger vergessener Tenant-Filter in einem tief verschachtelten Resolver ausreicht, um Daten über Mandantengrenzen hinweg preiszugeben. Andererseits bietet GraphQL mit dem Context-Objekt einen zentralen, für jeden Resolver verfügbaren Ort, an dem Tenant-Informationen einmal aufgelöst und danach konsistent wiederverwendet werden können, was bei sauberer Architektur robuster ist als verstreute Tenant-Checks in REST-Controllern.

2. Drei Architekturmodelle: Shared Schema, Schema-per-Tenant, Hybrid

Das Shared-Schema-Modell betreibt ein einziges Schema und eine einzige Server-Instanz für alle Mandanten, wobei jede Query im Resolver anhand des Tenant-Kontexts gefiltert wird. Dieses Modell skaliert am einfachsten, weil Deployments, Monitoring und Schema-Evolution für alle Mandanten gleichzeitig passieren, verlangt aber diszipliniertes Tenant-Scoping in jedem einzelnen Resolver, ohne Ausnahme.

Das Schema-per-Tenant-Modell erzeugt für jeden Mandanten eine eigene Server-Instanz oder zumindest ein eigenes generiertes Schema, oft mit Feldern, die nur für bestimmte Tenants sichtbar sind. Dieses Modell eignet sich für Enterprise-Kunden mit stark abweichenden Anforderungen, verursacht aber erheblich mehr Betriebsaufwand. Ein Hybrid-Modell kombiniert beides: ein gemeinsames Kern-Schema für alle Mandanten, ergänzt um mandantenspezifische Erweiterungen über Schema Stitching, was GraphQL Multi-Tenancy für die meisten SaaS-Produkte am pragmatischsten macht.

3. Tenant-Kontext im Request: Header, JWT und Context-Objekt

Der Tenant muss aus jedem eingehenden Request eindeutig identifizierbar sein, üblicherweise über einen Custom Header wie X-Tenant-ID, über eine Subdomain, oder, sicherer, direkt aus dem JWT-Access-Token, das der Client bei der Authentifizierung erhält. Ein JWT-basierter Ansatz verhindert, dass ein Client durch Manipulation eines Headers versucht, sich als ein anderer Mandant auszugeben, weil die Tenant-ID Teil der signierten, serverseitig verifizierten Token-Payload ist statt eines frei änderbaren Request-Headers.

Diese Tenant-ID wird in der context-Funktion des GraphQL-Servers genau einmal pro Request aufgelöst und anschließend jedem Resolver über das Context-Objekt zur Verfügung gestellt. Wichtig ist, diese Auflösung so früh wie möglich zu validieren: Ein ungültiges oder fehlendes Tenant-Token sollte die Anfrage bereits vor der Query-Ausführung mit einem Authentifizierungsfehler beenden, statt erst in einem tief verschachtelten Resolver aufzufallen.


// context.js — resolve tenant once per request, not per resolver
const { verifyJwt } = require('./auth');

async function createContext({ req }) {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) {
    throw new Error('Missing authentication token');
  }

  // tenantId comes from the signed JWT payload, never from a client-set header
  const { tenantId, userId } = await verifyJwt(token);

  return {
    tenantId,
    userId,
    // Every resolver receives a data source already scoped to this tenant
    db: getScopedDataSource(tenantId),
  };
}

module.exports = { createContext };

4. Resolver-Ebene: Tenant-Isolation ohne Code-Duplikation

Der naheliegende, aber fehleranfällige Ansatz ist, in jedem einzelnen Resolver manuell WHERE tenant_id = ? zu ergänzen. Bei hunderten Resolvern ist es nur eine Frage der Zeit, bis ein neuer Resolver diesen Filter vergisst, meist unter Zeitdruck bei einem neuen Feature. GraphQL Multi-Tenancy sollte diesen Filter deshalb strukturell erzwingen, statt ihn der Disziplin einzelner Entwickler zu überlassen.

Ein bewährtes Muster ist ein tenant-scoped Data Access Layer, der bereits mit der Tenant-ID aus dem Context instanziiert wird und dessen Methoden gar keine Möglichkeit bieten, diesen Filter zu umgehen. Resolver rufen ausschließlich Methoden dieses Layers auf und haben strukturell keinen Zugriff auf eine ungefilterte Datenbankverbindung, wodurch ein vergessener Tenant-Filter als Fehlerklasse komplett entfällt, statt nur unwahrscheinlicher zu werden.


// resolvers/product.js — resolvers never see an unscoped db connection
const resolvers = {
  Query: {
    // context.db is already tenant-scoped, no manual filtering needed here
    products: (_parent, args, context) => context.db.products.findMany(args),
  },
  Mutation: {
    createProduct: (_parent, { input }, context) =>
      context.db.products.create(input),
  },
};

module.exports = { resolvers };

5. Datenzugriff absichern: Row-Level-Security und Query-Scoping

Der Data Access Layer aus dem vorherigen Abschnitt ist eine Anwendungsebene-Absicherung, die bei einem Bug in genau diesem Layer versagen kann. Als zusätzliche, unabhängige Sicherheitsebene bieten Datenbanken wie PostgreSQL native Row-Level-Security-Policies, die den Tenant-Filter direkt auf Datenbankebene erzwingen, unabhängig davon, welche Anwendungslogik die Query absetzt.

Mit einer RLS-Policy, die current_setting('app.tenant_id') gegen die tenant_id-Spalte jeder Zeile prüft, kann selbst eine fehlerhafte Anwendungs-Query keine Zeilen anderer Mandanten zurückgeben, weil die Datenbank selbst die Filterung erzwingt. Diese Verteidigung in der Tiefe, Anwendungsebene plus Datenbankebene, ist bei sensiblen Mandantendaten, etwa im Finanz- oder Gesundheitsbereich, keine Kür mehr, sondern in vielen Compliance-Frameworks eine Pflichtanforderung.


-- PostgreSQL Row-Level Security as a database-level safety net
ALTER TABLE products ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON products
  USING (tenant_id = current_setting('app.tenant_id')::uuid);

-- The application sets this once per connection/request
-- SET app.tenant_id = '3f29...';

6. Tenant-spezifische Schema-Erweiterungen mit Schema Stitching

Manche Mandanten benötigen zusätzliche Felder oder ganze Typen, die für andere Mandanten irrelevant oder sogar unerwünscht sind, etwa branchenspezifische Zusatzattribute bei einem Enterprise-Kunden. Ein vollständig getrenntes Schema pro Mandant wäre für diesen Fall überdimensioniert. Schema Stitching oder Apollo Federation erlauben es stattdessen, ein gemeinsames Kern-Schema um mandantenspezifische Subgraphen zu erweitern, die nur für bestimmte Tenants aktiviert werden.

In der Praxis bedeutet das: Das Gateway prüft anhand des Tenant-Kontexts, welche Subgraphen für die aktuelle Anfrage relevant sind, und leitet die entsprechenden Teile der Query gezielt weiter. Für die überwiegende Mehrheit der Mandanten bleibt das Schema schlank und übersichtlich, während einzelne Enterprise-Kunden erweiterte Funktionalität erhalten, ohne dass sich das Basis-Schema für alle anderen Mandanten verändert.

7. Rate Limiting und Query Complexity pro Mandant

Ohne mandantenspezifisches Rate Limiting kann ein einzelner Tenant mit besonders komplexen oder häufigen Queries die gesamte Infrastruktur für alle anderen Mandanten verlangsamen, ein klassisches "Noisy Neighbor"-Problem. GraphQL Multi-Tenancy sollte deshalb Query-Complexity-Limits und Rate Limits nicht global, sondern pro Tenant-ID durchsetzen, sodass ein einzelner Mandant sein eigenes Kontingent überschreiten kann, ohne andere zu beeinträchtigen.

Ein Rate-Limiting-Middleware-Layer, der die Tenant-ID aus dem Context liest und pro Tenant ein eigenes Zeitfenster-Kontingent in Redis führt, lässt sich unabhängig vom eigentlichen Business-Schema implementieren. Enterprise-Mandanten mit vertraglich vereinbarter höherer Kapazität erhalten dabei einfach ein höheres Kontingent im selben Mechanismus, statt einer komplett separaten Infrastruktur.


// middleware/rate-limit.js — per-tenant quota instead of a global limit
const { RateLimiterRedis } = require('rate-limiter-flexible');

const limiter = new RateLimiterRedis({
  storeClient: redisClient,
  keyPrefix: 'graphql-rate-limit',
  points: 1000,   // requests per window, per tenant
  duration: 3600, // one hour window
});

async function enforceRateLimit(context) {
  try {
    // Each tenant consumes only from its own bucket
    await limiter.consume(context.tenantId);
  } catch {
    throw new Error('Rate limit exceeded for this tenant');
  }
}

module.exports = { enforceRateLimit };

8. Testing und Debugging mandantenfähiger Resolver

Automatisierte Tests für GraphQL Multi-Tenancy müssen explizit prüfen, dass Tenant A niemals Daten von Tenant B in der Antwort sieht, nicht nur, dass die Query im generischen Fall korrekte Ergebnisse liefert. Ein bewährtes Testmuster: Für jeden kritischen Resolver wird ein Testfall angelegt, der zwei Mandanten mit überlappenden IDs oder ähnlichen Datensätzen anlegt und explizit verifiziert, dass eine Query im Kontext von Tenant A ausschließlich Daten von Tenant A zurückgibt.

Beim Debugging in Produktion ist strukturiertes Logging mit der Tenant-ID als Pflichtfeld in jedem Log-Eintrag entscheidend, weil sich sonst Isolationsfehler im Nachhinein kaum rekonstruieren lassen. Ein Correlation-ID-System, das Tenant-ID, User-ID und Request-ID über den gesamten Resolver-Baum hinweg mitführt, verkürzt die Fehlersuche bei gemeldeten Datenlecks erheblich gegenüber grep-basierter Log-Analyse ohne strukturierte Felder.

9. Multi-Tenancy-Architekturen im Vergleich

Die Wahl der richtigen Architektur hängt stark von Mandantenzahl, Compliance-Anforderungen und dem Grad an mandantenspezifischer Individualisierung ab.

Modell Isolation Betriebsaufwand Geeignet für
Shared Schema Anwendungsebene + optional RLS Gering, ein Deployment Viele kleine bis mittlere Mandanten
Schema-per-Tenant Vollständig getrennte Instanzen Hoch, N Deployments Wenige Enterprise-Kunden, strenge Compliance
Hybrid mit Schema Stitching Kern-Schema geteilt, Erweiterungen isoliert Mittel SaaS mit wenigen Enterprise-Ausnahmen

Für die meisten SaaS-Produkte ist das Shared-Schema-Modell mit Row-Level-Security als zusätzlicher Absicherung der pragmatischste Startpunkt. Schema-per-Tenant lohnt sich erst, wenn einzelne Großkunden vertraglich physische Datentrennung verlangen, nicht als vorsorgliche Architekturentscheidung für ein neues Produkt.

Mironsoft

GraphQL-SaaS-Architektur, Tenant-Isolation und Schema-Design

Eine gemeinsame API, die Mandanten wirklich trennt?

Wir bauen tenant-scoped Data Access Layer, richten Row-Level-Security ein und implementieren mandantenspezifisches Rate Limiting, damit ein Isolationsfehler strukturell unmöglich wird statt nur unwahrscheinlich.

Architektur-Review

Bestehende Resolver auf Tenant-Isolationslücken prüfen

RLS-Implementierung

Row-Level-Security als datenbankseitige Verteidigungsschicht einrichten

Rate Limiting

Mandantenspezifische Kontingente gegen Noisy-Neighbor-Effekte

10. Zusammenfassung

GraphQL Multi-Tenancy lässt sich nicht an einer einzelnen Stelle im Code lösen, weil eine Query in beliebiger Tiefe durch mehrere Resolver wandert. Der zuverlässige Ansatz kombiniert einen tenant-scoped Data Access Layer, der Tenant-Filter strukturell erzwingt, mit Row-Level-Security als unabhängiger Sicherheitsebene direkt in der Datenbank. Das Shared-Schema-Modell ist für die meisten SaaS-Produkte der pragmatische Ausgangspunkt, ergänzt um Schema Stitching für die wenigen Mandanten mit abweichenden Anforderungen.

Rate Limiting pro Tenant-ID verhindert, dass ein einzelner Mandant die gemeinsame Infrastruktur für alle anderen verlangsamt, und explizite Isolationstests, die zwei Mandanten mit überlappenden Datensätzen prüfen, decken vergessene Tenant-Filter auf, bevor sie in Produktion zu einem echten Datenleck werden.

GraphQL Multi-Tenancy — Das Wichtigste auf einen Blick

Tenant-Kontext

Tenant-ID einmal pro Request aus dem JWT auflösen und über das Context-Objekt an alle Resolver weitergeben.

Strukturelle Isolation

Tenant-scoped Data Access Layer statt manueller WHERE-Klauseln in jedem Resolver.

Verteidigung in der Tiefe

Row-Level-Security auf Datenbankebene als unabhängiges Backup zur Anwendungslogik.

Fairness zwischen Mandanten

Rate Limiting pro Tenant-ID verhindert Noisy-Neighbor-Effekte auf gemeinsamer Infrastruktur.

11. FAQ: GraphQL Multi-Tenancy

1Was bedeutet Multi-Tenancy bei GraphQL?
Eine gemeinsame API bedient mehrere Mandanten, wobei jeder Mandant nur eigene Daten sieht, durchgesetzt auf jeder Resolver-Ebene.
2Warum ist Isolation in GraphQL schwieriger als in REST?
Eine Query wandert verschachtelt durch mehrere Resolver, ein vergessener Filter irgendwo reicht für ein Datenleck.
3Wie identifiziere ich den Mandanten im Request?
Am sichersten aus der signierten JWT-Payload, statt aus einem frei manipulierbaren Header.
4Was ist ein tenant-scoped Data Access Layer?
Eine Zugriffsschicht mit fest eingebautem Tenant-Filter, die Resolver nicht umgehen können.
5Was ist Row-Level-Security?
Datenbankseitige Erzwingung des Tenant-Filters, unabhängig von der Anwendungslogik, als zusätzliche Sicherheitsebene.
6Wann lohnt sich Schema-per-Tenant?
Bei vertraglich verlangter physischer Datentrennung. Für die meisten SaaS-Produkte ist Shared Schema mit RLS pragmatischer.
7Wie füge ich mandantenspezifische Felder hinzu?
Über Schema Stitching oder Apollo Federation mit mandantenspezifischen Subgraphen.
8Wie verhindere ich Noisy-Neighbor-Effekte?
Mit Rate Limiting und Query-Complexity-Limits pro Tenant-ID statt eines globalen Limits.
9Wie teste ich Tenant-Isolation zuverlässig?
Mit expliziten Tests, die zwei Mandanten mit überlappenden Daten anlegen und Kreuzzugriffe verifizieren.
10Was gehört ins Logging mandantenfähiger Resolver?
Die Tenant-ID als Pflichtfeld in jedem Log-Eintrag, idealerweise als Teil eines Correlation-ID-Systems.