Multi-Tenant SaaS Architecture: Der vollständige Leitfaden für 2026

Wie Sie eine Multi-Tenant-SaaS-Architektur entwerfen – Tenant-Isolationsmodelle, Identifikation, zeilenbasierte Bereichsdefinition, tenantübergreifende Analysen, Migrationen – verankert in 9 Produktions-Apps.

Huifer
Huifer
14. Juli 202611 min read
Auch verfügbar auf:English · 中文

Geschrieben von Huifer, Solo-Entwickler und Maintainer von TanStack Ship. In neun produktiven Multi-Tenant-SaaS-Apps – darunter ein Multi-Tenant-Analytics-Tool mit ~140.000 Requests/Tag, zwei B2B-Billing-Plattformen, ein Workspace-Kollaborationstool und fünf kleinere Apps – habe ich die Patterns ausgeliefert, die darüber entscheiden, ob eine Tenant-Grenze unter echtem Traffic standhält. Dieser Leitfaden konsolidiert diese Patterns: Isolationsmodelle, Tenant-Identifikation, zeilenbasierte Bereichsdefinition, tenantübergreifende Belange und Migrationspfade. Jedes Pattern unten ist in ausgeliefertem Code. Die kürzere Version finden Sie im Multi-Tenant-Architektur-Leitfaden.

Verifizierte Quellen: Cloudflare D1-Dokumentation · Cloudflare Workers-Dokumentation · SQLite Query Planner · TanStack Router-Dokumentation · TanStack Ship GitHub Organization · TanStack Ship Multi-Tenant-Referenz-Repo

Letzte Aktualisierung: 2026-07-14 · Änderungsprotokoll


Kurzfassung: Multi-Tenant-SaaS-Architektur ist die Disziplin, Kundendaten zu isolieren und gleichzeitig Infrastruktur zu teilen. Für ~95 % der SaaS-Produkte ist das richtige Modell die zeilenbasierte Isolation: eine Datenbank, ein Schema, eine tenant_id-Spalte in jeder Tabelle und ein Tenant-Kontext, der am Edge injiziert wird. Dieser Leitfaden durchläuft die drei Isolationsmodelle, die Tenant-Identifikation, die zeilenbasierte Disziplin, die Datenlecks verhindert, tenantübergreifende Analysen und Migrationspfade. Wenn Sie einen Stack wählen, lesen Sie die TanStack Ship Features-Seite; für den breiteren Kontext den SaaS-Architektur-Leitfaden 2026.


Die drei Isolationsmodelle und wann jedes seine Komplexität rechtfertigt

Die ehrliche Empfehlung: Zeilenbasiert für die meisten Apps

Jede Multi-Tenant-Entscheidung beginnt mit denselben drei Optionen: separate Datenbanken pro Tenant, separate Schemas pro Tenant oder gemeinsame Tabellen mit einer tenant_id-Spalte. Die Versuchung ist, mit der am stärksten isolierten Option zu beginnen, weil sie sich sicherer anfühlt. Widerstehen Sie. Die am stärksten isolierte Option ist auch die teuerste im Betrieb, die langsamste bei der Migration und die schwierigste, um sie im Maßstab zu reasoningen. Laut der Cloudflare D1-Dokumentation unterstützt D1 mehrere Datenbanken pro Account; die Frage ist, ob Sie diese Komplexität am Tag eins wollen.

Das Modell, das ich für ~95 % der SaaS-Produkte ausliefere, ist zeilenbasierte Isolation: eine Datenbank, ein Schema, jede Domain-Tabelle beginnt mit tenant_id, jede Query wird nach Tenant eingeschränkt und der Tenant-Kontext wird am Edge injiziert, bevor eine Serverfunktion läuft. Eine Datenbank bedeutet eine Verbindung, eine Migration, ein Backup. Ein Schema bedeutet, dass EXPLAIN für jeden Tenant gleich funktioniert. Ein Tabellensatz bedeutet, dass tenantübergreifende Analysen eine einzelne SQL-Query sind. Die beiden anderen Modelle haben legitime Anwendungsfälle – regulierte Branchen, Enterprise-Kunden, die physische Isolation fordern, sehr große Tenants, deren Datenvolumen alle anderen in den Schatten stellt. Der Weg zu diesen Modellen ist gut begangen; der Weg zurück ist schmerzhaft. Starten Sie einfach.

Wann Datenbank-pro-Tenant seine Komplexität rechtfertigt

Datenbank-pro-Tenant gibt jedem Tenant seine eigene D1-Datenbank; die Anwendung routet Requests basierend auf dem Tenant-Kontext. Vorteile: physische Isolation, per-Tenant-Backup und -Restore, die Möglichkeit, großen Tenants ihre eigene Read-Replica-Topologie zu geben. Nachteile: jede zusätzliche Datenbank ist eine weitere Bindung, eine weitere Migration, eine weitere Abrechnungszeile. Ehrliche Anwendungsfälle, die ich ausgeliefert habe: eine Healthcare-Plattform, bei der ein Kunde HIPAA-konforme per-Tenant-Datenresidenz erforderte; eine B2B-Plattform mit einem Kunden, der ~40 % des Write-Volumens generierte (das Aufsplitten dieses Kunden reduzierte die Write-Lock-Kontention für alle anderen); eine White-Label-Plattform, bei der jede Instanz als separates Produkt verkauft wurde. Wenn nichts davon zutrifft, greifen Sie nicht dazu – der Overhead wächst linear mit der Tenant-Anzahl.

Schema-pro-Tenant als Mittelweg, der selten seine Berechtigung hat

Separate Schemas pro Tenant in einer einzigen Datenbank klingt ansprechend: physische Isolation, eine Verbindung, eine Migration. In der Praxis bietet Schema-pro-Tenant in SQLite (oder D1) nicht das, was Postgres-Nutzer erwarten. Der Query Planner optimiert nicht über Schemas hinweg, wie er es über Tabellen tut; schemasubergreifende Joins sind langsam; partielle Indizes übertragen sich nicht. Schema-pro-Tenant rechtfertigt seinen Platz in Postgres-gestützten Apps, in denen search_path-Wechsel gut verstanden ist – ein anderer Stack. Der Rest dieses Leitfadens nimmt zeilenbasierte Isolation an; die Patterns übertragen sich direkt auf Schema-pro-Tenant (ersetzen Sie tenant_id durch schema_name) und Datenbank-pro-Tenant (ersetzen Sie tenant_id durch database_name).

Tenant-Identifikation: drei Patterns, eine Regel

Die einzelne Regel: Lösen Sie den Tenant auf, bevor Geschäftslogik läuft

Die erste architektonische Entscheidung ist, wie die Anfrage ihren Tenant identifiziert. Die zweite – und wichtigere – ist, wann diese Identifikation stattfindet. Die einzige Regel, mit der ich ausliefere: Lösen Sie den Tenant auf, bevor Geschäftslogik läuft. Der Tenant-Kontext fließt als ambienter Zustand durch die Anfrage; jede Serverfunktion, jeder Loader, jeder Webhook-Handler liest ihn, ohne ihn erneut abzuleiten. Drei Identifikationspatterns decken ~99 % der SaaS-Apps ab: Subdomain (acme.example.com – der Host-Header trägt den Slug), Path-Präfix (example.com/t/acme/dashboard – das erste Path-Segment trägt den Slug, TanStack Routers verschachteltes Layout macht den Kontext für jede verschachtelte Route verfügbar) und Auth-Token (der Tenant lebt in der Session, nützlich für B2B-Apps, in denen der Benutzer zu einem Workspace gehört). Subdomain ist am saubersten, weil jede Anfrage eine Tenant-Anfrage ist. Path-Präfix ist am flexibelsten, weil es auf einer einzigen Domain funktioniert. Auth-Token ist der Fallback für White-Label-Apps.

Subdomain-Auflösung am Edge

Das Pattern auf Cloudflare Workers löst den Host-Header gegen eine tenants-Lookup auf und gibt den Tenant als ambienten Kontext zurück:

typescript
// src/middleware/tenant.ts
export const tenantMiddleware = defineMiddleware({
  before: async ({ request }) => {
    const host = request.headers.get('host') ?? ''
    const slug = host.split('.')[0]
    if (['www', 'api', 'app'].includes(slug)) return
    const tenant = await db
      .prepare('SELECT id, plan, status FROM tenants WHERE slug = ?')
      .bind(slug).first<Tenant>()
    if (!tenant || tenant.status !== 'active') {
      return new Response('Tenant not found', { status: 404 })
    }
    return { tenantId: tenant.id, tenantPlan: tenant.plan }
  },
})

Die Middleware läuft vor jeder Serverfunktion; das zurückgegebene Objekt wird zum ambienten Kontext – jeder Loader, jede Mutation und jeder Webhook-Handler liest context.tenantId ohne einen zweiten Lookup. TanStack Router nimmt dies automatisch auf.

Path-Präfix-Auflösung mit Route Guards

Wenn Subdomains nicht möglich sind – lokale Entwicklung, ein einzelnes Staging-Environment, ein path-first-Produkt – nutzt das Path-Präfix-Pattern TanStack Routers verschachtelte Layouts. Ein _layout.tsx unter routes/t/$tenantSlug/ löst den Slug auf, leitet bei Fehlschlag um und gibt den Tenant-Kontext zurück. Das $tenantSlug-Segment ist ein Route-Parameter, kein Query-String – es überlebt Page Reloads und ist shareable. Der TanStack Router Advanced Patterns Guide behandelt die Konventionen.

Zeilenbasierte Isolation: Die vier Disziplinen

Jede Domain-Tabelle beginnt mit tenant_id

Die erste Disziplin ist mechanisch: Jede Domain-Tabelle hat eine tenant_id-Spalte als erste Spalte, jeder zusammengesetzte Index hat tenant_id als erste Spalte, jeder Fremdschlüssel referenziert zurück auf den Tenant. Der Grund ist die Leftmost-Prefix-Regel des SQLite Query Planners: Ein zusammengesetzter Index (tenant_id, created_at) bedient WHERE tenant_id = ? ORDER BY created_at DESC ohne Sortierung. Laut der SQLite Query Planner-Dokumentation nutzt der Planner diesen Index effizient und ignoriert ihn für Queries, die tenant_id auslassen – genau das, was Sie für eine Datenbank wollen, in der jede Query nach Tenant filtern muss. Der partielle Index auf archived_at IS NULL ist die SQLite-spezifische Optimierung, die den Index klein hält, während die Tabelle wächst. Das vollständige D1 Schema-Design-Playbook finden Sie im D1 Production Guide.

Jede Query ist eingeschränkt und das Typsystem erzwingt es

Die zweite Disziplin verhindert tatsächlich Datenlecks: Jede Query wird nach Tenant eingeschränkt und das Typsystem macht eine uneingeschränkte Query zu einem Kompilierfehler. Das Pattern ist ein dünner Wrapper, der tenantId als erforderliches Argument nimmt und einen Query Builder zurückgibt, bei dem tenant_id in jede Klausel eingebrannt ist:

typescript
// src/lib/tenant-db.ts
export function tenantDb(tenantId: string) {
  return {
    projects: {
      list: () => db.prepare(
        'SELECT * FROM projects WHERE tenant_id = ? AND archived_at IS NULL ORDER BY created_at DESC'
      ).bind(tenantId),
      insert: (name: string) => db.prepare(
        'INSERT INTO projects (id, tenant_id, name) VALUES (?, ?, ?)'
      ).bind(crypto.randomUUID(), tenantId, name),
    },
  }
}
// Jede Serverfunktion ruft tenantDb(context.tenantId) auf.
// Es gibt kein globales `db.projects` – nur den eingeschränkten Wrapper.

Der Compilezeit-Vorteil: Kein globales db.projects, das man vergessen könnte. Der Runtime-Vorteil: Jede Query, die die Datenbank erreicht, hat bereits den Tenant-Filter. Typsystem + ambianter Kontext + kein uneingeschränktes Global – diese Kombination macht zeilenbasierte Isolation im Maßstab sicher.

Soft Delete mit Audit Trail

Harte Deletes sind eine Stolperfalle: Ein Benutzer klickt auf „Projekt löschen", die Row verschwindet, die nächste Rechnung referenziert noch die Projekt-ID und der Fremdschlüsselverstoß feuert in der Produktion. Die Lösung ist Soft Delete mit einer Audit-Tabelle – jeder Delete fügt in project_deletions ein und aktualisiert projects.deleted_at innerhalb einer einzigen db.batch(); Reads filtern auf deleted_at IS NULL; ein nächtlicher Cron hard-deleted Rows älter als 90 Tage nach Export nach R2. Das vollständige D1-Migrations-Playbook finden Sie im D1 Deep-Dive Guide.

Rate-Limits, Kontingente und Plan-Grenzen sind tenant-bereichsdefiniert

Die vierte Disziplin verhindert den peinlichsten Produktionsvorfall: Rate-Limits werden pro Tenant bereichsdefiniert, nicht pro IP. Ein einzelner Tenant kann tausend gleichzeitige Syncs ausführen, ohne ein globales Limit auszulösen; ein Noisy Neighbor kann keinen gemeinsamen Bucket erschöpfen und alle anderen aushungern. Der Rate-Limit-Schlüssel ist rl:${tenantId}:${route}, damit jeder Tenant seinen eigenen Bucket bekommt. Die Plan-Grenze – free 60 req/min, pro 600 req/min, enterprise unbegrenzt – wird aus tenantPlan im ambienten Kontext gelesen. Plan-Änderungen erfordern keine Code-Änderungen; der Plan lebt in der Datenbank. Das vollständige D1 operative Playbook finden Sie im D1 Production Guide.

Tenantübergreifende Belange: Die Arbeit, die in einer Datenbank einfacher ist

Analysen, Billing und die Queries, die jeden Tenant brauchen

Die Arbeit, die in zeilenbasierter Isolation genuin einfacher ist, sind tenantübergreifende Analysen. Eine einzelne SQL-Query joined über jeden Tenant; kein Fanout, kein per-Tenant-Job, keine Aggregations-Pipeline. Eine Monthly Recurring Revenue-Query ist ein GROUP BY plan gegen dieselben Indizes, die die tenant-servicenden Queries bereits nutzen; keine spezielle Analytics-Pipeline, kein separates Warehouse, kein ETL-Job. Die vollständige Observability-Form finden Sie im SaaS Monitoring and Observability Guide.

Das Admin-Superuser-Pattern: Eine separate Auth-Grenze

Die Arbeit, die Sorgfalt erfordert, ist das Admin-Superuser-Pattern: Der Operator, der sich einmal einloggt und jeden Tenant sieht. Behandeln Sie Admin als separate Auth-Grenze, nicht als speziellen Tenant. Admin-Serverfunktionen umgehen den tenantDb-Wrapper und rufen die raw db direkt auf; jede Admin-Aktion wird mit actor: 'admin:<user_id>' geloggt, für das Audit. Die Versuchung, einen „Super Tenant" mit der ID * hinzuzufügen, ist eine Stolperfalle – es bricht die Disziplin, dass jede Query bereichsdefiniert ist, und das erste Mal, wenn jemand vergisst zu filtern, leaken alle Tenant-Daten in die Admin-Ansicht. Admin-Routen importieren die raw db und schreiben jede Query mit expliziten WHERE tenant_id = ?-Klauseln.

Migrationen und der Weg aus der einfachen Form

Die Single-Tenant-zu-Multi-Tenant-Migration

Der Weg, den ich zweimal ausgeliefert habe: Fügen Sie eine tenants-Tabelle hinzu und fügen Sie den Originalkunden mit einer stabilen ID wie original ein; fügen Sie tenant_id zu jeder Domain-Tabelle hinzu und backfillen Sie mit original; wrappen Sie jede Serverfunktion in den tenantDb-Wrapper mit der Original-ID, gelesen aus einem Config-Flag; bauen Sie den Tenant-Signup-Flow, damit neue Kunden eine frische ID bekommen; benennen Sie die Original-ID als einmalige Migration in einen echten Slug um. Circa zwei Wochen für eine mittelgroße App. Der Fehler, den ich zweimal gemacht habe: den Wrapper-Schritt zu überspringen – die uneingeschränkte db-globale „nur für die Migration" zu belassen. Jede uneingeschränkte Globale, die überlebt, ist ein zukünftiges Datenleck.

Der Datenbank-pro-Tenant-Split, wenn ein Tenant die gemeinsame Form überwächst

Die zweite Migration ist der Split: Das Datenvolumen eines Tenants stellt alle anderen in den Schatten, Write-Lock-Kontention schadet der gemeinsamen Datenbank. Kopieren Sie die Rows dieses Tenants in eine neue Datenbank, richten Sie eine separate D1-Bindung ein, routen Sie Requests für diesen Tenant auf der Middleware-Ebene zur neuen Datenbank. Ein Wochenende Arbeit; der Unsplit – zurück zu einer Datenbank – ist ein Rewrite. Operative Lektion: Designen Sie die Routing-Schicht von Anfang an für Split-Fähigkeit, auch wenn kein Tenant groß genug ist, um den Split heute zu brauchen. Jeder Datenbankzugriff geht durch eine getDb(tenantId)-Funktion; jede Serverfunktion ruft getDb(context.tenantId) statt des globalen Imports auf. Der Tag, an dem der Split notwendig wird, ist eine Config-Änderung, kein Rewrite.

Wo dieser Leitfaden aufhört

Dies ist die Form der Multi-Tenant-SaaS-Architektur im Jahr 2026: zeilenbasierte Isolation für ~95 % der Apps, Tenant-Identifikation am Edge, vier Disziplinen, die Datenlecks verhindern, tenantübergreifende Analysen, die nichts extra kosten, ein Migrationspfad von einem Tenant zu tausend. Die Form ist langlebig; die Primitiven sind ersetzbar. Tauschen Sie D1 gegen Postgres aus und die zeilenbasierte Disziplin überträgt sich. Tauschen Sie Workers gegen einen Node-Server aus und das Middleware-Pattern überträgt sich. Die geschäftliche Seite – Preise, Enterprise-Verträge, Onboarding, Compliance – wird hier nicht behandelt.


Abschließende CTA: TanStack Ship liefert jedes Multi-Tenant-Pattern in diesem Leitfaden über neun SaaS-Apps aus. Siehe die Features-Seite, vergleichen Sie mit Alternativen oder lesen Sie den SaaS-Architektur-Leitfaden 2026 für den breiteren Kontext. Der D1 Deep-Dive behandelt die Datenebene; die TanStack Router Advanced Patterns durchlaufen die Routing-Formen.