# Multi-Tenancy Architecture **Meta-Description:** Multi-Tenant SaaS Architektur. Database Isolation, Tenant Context, Row-Level Security und Scaling Strategien. **Keywords:** Multi-Tenancy, SaaS Architecture, Database Isolation, Row-Level Security, Tenant, B2B, Scaling --- ## Einführung **Multi-Tenancy** ermöglicht es, eine Anwendung für viele Kunden (Tenants) zu betreiben. Von **Database per Tenant** bis **Row-Level Security** – die richtige Strategie hängt von Sicherheit, Kosten und Skalierung ab. --- ## Multi-Tenancy Overview ``` ┌─────────────────────────────────────────────────────────────┐ │ MULTI-TENANCY STRATEGIES │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. DATABASE PER TENANT: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ [App] → [Tenant A DB] [Tenant B DB] [Tenant C DB] │ │ │ │ │ │ │ │ ✓ Maximum isolation │ │ │ │ ✓ Easy data migration │ │ │ │ ✗ Higher infrastructure cost │ │ │ │ ✗ Complex connection management │ │ │ │ Use case: Enterprise, regulated industries │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 2. SCHEMA PER TENANT: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ [Database] │ │ │ │ ├── schema_tenant_a │ │ │ │ ├── schema_tenant_b │ │ │ │ └── schema_tenant_c │ │ │ │ │ │ │ │ ✓ Good isolation │ │ │ │ ✓ Easier backup per tenant │ │ │ │ ✗ Schema migrations complexity │ │ │ │ Use case: Mid-size SaaS │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 3. SHARED DATABASE (Row-Level): │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ [Database] │ │ │ │ └── [Tables with tenant_id column] │ │ │ │ ├── users (tenant_id, ...) │ │ │ │ ├── projects (tenant_id, ...) │ │ │ │ └── documents (tenant_id, ...) │ │ │ │ │ │ │ │ ✓ Cost efficient │ │ │ │ ✓ Simple deployment │ │ │ │ ✗ Requires careful query design │ │ │ │ Use case: SMB SaaS, high tenant count │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 4. HYBRID APPROACH: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ - Shared DB for small tenants │ │ │ │ - Dedicated DB for enterprise tenants │ │ │ │ ✓ Flexible pricing tiers │ │ │ │ ✓ Meet enterprise requirements │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Tenant Context ```typescript // lib/tenant/context.ts import { AsyncLocalStorage } from 'async_hooks'; export interface TenantContext { tenantId: string; tenantSlug: string; plan: string; features: string[]; limits: Record; } const tenantStorage = new AsyncLocalStorage(); export function getTenantContext(): TenantContext { const context = tenantStorage.getStore(); if (!context) { throw new Error('Tenant context not initialized'); } return context; } export function runWithTenant( tenant: TenantContext, fn: () => T ): T { return tenantStorage.run(tenant, fn); } // Optional: Get tenant without throwing export function getTenantContextSafe(): TenantContext | null { return tenantStorage.getStore() || null; } ``` ```typescript // middleware.ts - Tenant Resolution import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export async function middleware(request: NextRequest) { const response = NextResponse.next(); // Strategy 1: Subdomain-based const hostname = request.headers.get('host') || ''; const subdomain = hostname.split('.')[0]; if (subdomain && subdomain !== 'www' && subdomain !== 'app') { response.headers.set('x-tenant-slug', subdomain); } // Strategy 2: Path-based (/org/tenant-slug/...) const pathname = request.nextUrl.pathname; const pathMatch = pathname.match(/^\/org\/([^\/]+)/); if (pathMatch) { response.headers.set('x-tenant-slug', pathMatch[1]); } // Strategy 3: Header-based (for API) const headerTenant = request.headers.get('x-tenant-id'); if (headerTenant) { response.headers.set('x-tenant-id', headerTenant); } return response; } // lib/tenant/resolver.ts import { db } from '@/lib/db'; import { TenantContext } from './context'; export async function resolveTenant( identifier: string, type: 'slug' | 'id' = 'slug' ): Promise { const tenant = await db.tenant.findUnique({ where: type === 'slug' ? { slug: identifier } : { id: identifier }, include: { subscription: true, features: true } }); if (!tenant) return null; return { tenantId: tenant.id, tenantSlug: tenant.slug, plan: tenant.subscription?.plan || 'free', features: tenant.features.map(f => f.name), limits: { users: tenant.subscription?.userLimit || 5, storage: tenant.subscription?.storageLimit || 1024, projects: tenant.subscription?.projectLimit || 10 } }; } ``` --- ## Database with Row-Level Security ```typescript // prisma/schema.prisma generator client { provider = "prisma-client-js" previewFeatures = ["multiSchema"] } model Tenant { id String @id @default(cuid()) slug String @unique name String createdAt DateTime @default(now()) // Relations users User[] projects Project[] subscription Subscription? } model User { id String @id @default(cuid()) email String name String? // Tenant relation tenantId String tenant Tenant @relation(fields: [tenantId], references: [id]) @@unique([tenantId, email]) @@index([tenantId]) } model Project { id String @id @default(cuid()) name String description String? // Tenant relation tenantId String tenant Tenant @relation(fields: [tenantId], references: [id]) // Other relations documents Document[] @@index([tenantId]) } model Document { id String @id @default(cuid()) title String content String? // Both tenant and project for redundant filtering tenantId String projectId String project Project @relation(fields: [projectId], references: [id]) @@index([tenantId]) @@index([projectId]) } ``` ```typescript // lib/db/tenant-client.ts import { PrismaClient } from '@prisma/client'; import { getTenantContext } from '@/lib/tenant/context'; // Extend Prisma with tenant filtering function createTenantPrismaClient() { const prisma = new PrismaClient(); return prisma.$extends({ query: { $allModels: { async findMany({ model, operation, args, query }) { const context = getTenantContext(); // Models that need tenant filtering const tenantModels = ['User', 'Project', 'Document', 'Team']; if (tenantModels.includes(model)) { args.where = { ...args.where, tenantId: context.tenantId }; } return query(args); }, async findFirst({ model, operation, args, query }) { const context = getTenantContext(); const tenantModels = ['User', 'Project', 'Document', 'Team']; if (tenantModels.includes(model)) { args.where = { ...args.where, tenantId: context.tenantId }; } return query(args); }, async create({ model, operation, args, query }) { const context = getTenantContext(); const tenantModels = ['User', 'Project', 'Document', 'Team']; if (tenantModels.includes(model)) { args.data = { ...args.data, tenantId: context.tenantId }; } return query(args); }, async update({ model, operation, args, query }) { const context = getTenantContext(); const tenantModels = ['User', 'Project', 'Document', 'Team']; if (tenantModels.includes(model)) { args.where = { ...args.where, tenantId: context.tenantId }; } return query(args); }, async delete({ model, operation, args, query }) { const context = getTenantContext(); const tenantModels = ['User', 'Project', 'Document', 'Team']; if (tenantModels.includes(model)) { args.where = { ...args.where, tenantId: context.tenantId }; } return query(args); } } } }); } export const tenantDb = createTenantPrismaClient(); ``` --- ## PostgreSQL Row-Level Security ```sql -- Enable RLS on tables ALTER TABLE users ENABLE ROW LEVEL SECURITY; ALTER TABLE projects ENABLE ROW LEVEL SECURITY; ALTER TABLE documents ENABLE ROW LEVEL SECURITY; -- Create policies CREATE POLICY tenant_isolation_users ON users USING (tenant_id = current_setting('app.current_tenant_id')::uuid); CREATE POLICY tenant_isolation_projects ON projects USING (tenant_id = current_setting('app.current_tenant_id')::uuid); CREATE POLICY tenant_isolation_documents ON documents USING (tenant_id = current_setting('app.current_tenant_id')::uuid); -- Function to set tenant context CREATE OR REPLACE FUNCTION set_tenant_context(tenant_id uuid) RETURNS void AS $$ BEGIN PERFORM set_config('app.current_tenant_id', tenant_id::text, false); END; $$ LANGUAGE plpgsql; ``` ```typescript // lib/db/rls-client.ts import { Pool } from 'pg'; import { getTenantContext } from '@/lib/tenant/context'; const pool = new Pool({ connectionString: process.env.DATABASE_URL }); export async function query( sql: string, params?: unknown[] ): Promise { const client = await pool.connect(); const context = getTenantContext(); try { // Set tenant context for RLS await client.query( "SELECT set_config('app.current_tenant_id', $1, true)", [context.tenantId] ); const result = await client.query(sql, params); return result.rows as T[]; } finally { client.release(); } } // Usage const projects = await query( 'SELECT * FROM projects WHERE status = $1', ['active'] ); // RLS automatically filters by tenant! ``` --- ## API Routes with Tenant ```typescript // app/api/projects/route.ts import { NextResponse } from 'next/server'; import { headers } from 'next/headers'; import { resolveTenant, runWithTenant } from '@/lib/tenant'; import { tenantDb } from '@/lib/db/tenant-client'; export async function GET(request: Request) { const headersList = await headers(); const tenantSlug = headersList.get('x-tenant-slug'); if (!tenantSlug) { return NextResponse.json({ error: 'Tenant not found' }, { status: 400 }); } const tenant = await resolveTenant(tenantSlug); if (!tenant) { return NextResponse.json({ error: 'Invalid tenant' }, { status: 404 }); } // Run with tenant context const projects = await runWithTenant(tenant, async () => { return tenantDb.project.findMany({ orderBy: { createdAt: 'desc' } }); }); return NextResponse.json(projects); } export async function POST(request: Request) { const headersList = await headers(); const tenantSlug = headersList.get('x-tenant-slug'); const tenant = await resolveTenant(tenantSlug!); if (!tenant) { return NextResponse.json({ error: 'Invalid tenant' }, { status: 404 }); } const body = await request.json(); // Check limits const projectCount = await runWithTenant(tenant, async () => { return tenantDb.project.count(); }); if (tenant.limits.projects !== -1 && projectCount >= tenant.limits.projects) { return NextResponse.json( { error: 'Project limit reached. Please upgrade your plan.' }, { status: 403 } ); } const project = await runWithTenant(tenant, async () => { return tenantDb.project.create({ data: { name: body.name, description: body.description } }); }); return NextResponse.json(project, { status: 201 }); } ``` --- ## Tenant Provisioning ```typescript // lib/tenant/provisioning.ts import { db } from '@/lib/db'; import { stripe } from '@/lib/stripe'; interface CreateTenantInput { name: string; slug: string; ownerEmail: string; ownerName: string; plan?: string; } export async function provisionTenant(input: CreateTenantInput) { // Validate slug const existingTenant = await db.tenant.findUnique({ where: { slug: input.slug } }); if (existingTenant) { throw new Error('Tenant slug already exists'); } // Create Stripe customer const stripeCustomer = await stripe.customers.create({ email: input.ownerEmail, name: input.name, metadata: { tenantSlug: input.slug } }); // Create tenant with owner in transaction const tenant = await db.$transaction(async (tx) => { // Create tenant const newTenant = await tx.tenant.create({ data: { name: input.name, slug: input.slug, stripeCustomerId: stripeCustomer.id, settings: { create: { theme: 'light', timezone: 'Europe/Berlin' } } } }); // Create owner user await tx.user.create({ data: { email: input.ownerEmail, name: input.ownerName, tenantId: newTenant.id, role: 'owner' } }); // Create default resources await tx.project.create({ data: { name: 'My First Project', tenantId: newTenant.id } }); return newTenant; }); // Send welcome email await sendWelcomeEmail(input.ownerEmail, tenant.slug); return tenant; } // Tenant deletion (with data cleanup) export async function deprovisionTenant(tenantId: string) { const tenant = await db.tenant.findUnique({ where: { id: tenantId }, include: { subscription: true } }); if (!tenant) { throw new Error('Tenant not found'); } // Cancel Stripe subscription if (tenant.subscription?.stripeSubscriptionId) { await stripe.subscriptions.cancel(tenant.subscription.stripeSubscriptionId); } // Delete all tenant data in transaction await db.$transaction([ db.document.deleteMany({ where: { tenantId } }), db.project.deleteMany({ where: { tenantId } }), db.user.deleteMany({ where: { tenantId } }), db.subscription.deleteMany({ where: { tenantId } }), db.tenantSettings.deleteMany({ where: { tenantId } }), db.tenant.delete({ where: { id: tenantId } }) ]); // Archive data (optional) await archiveTenantData(tenantId); } ``` --- ## Strategy Comparison | Strategy | Isolation | Cost | Complexity | Best For | |----------|-----------|------|------------|----------| | **DB per Tenant** | Highest | High | High | Enterprise | | **Schema per Tenant** | High | Medium | Medium | Mid-market | | **Shared + RLS** | Medium | Low | Medium | SMB/Startup | | **Hybrid** | Flexible | Variable | High | Mixed segments | --- ## Fazit Multi-Tenancy Architektur erfordert: 1. **Isolation Strategy**: Passend zum Use Case wählen 2. **Tenant Context**: Durchgängig in der Anwendung 3. **Security**: RLS oder Application-Level Filtering 4. **Provisioning**: Automatisierte Tenant-Erstellung Die richtige Strategie hängt von Kunden und Skalierung ab. --- ## Bildprompts 1. "Multi-tenant architecture diagram, database isolation strategies" 2. "Tenant provisioning flow, signup to activation" 3. "Row-level security visualization, data filtering per tenant" --- ## Quellen - [Microsoft Multi-Tenancy Patterns](https://docs.microsoft.com/en-us/azure/architecture/guide/multitenant/overview) - [PostgreSQL RLS Documentation](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) - [Prisma Multi-Tenancy](https://www.prisma.io/docs/guides/other/multi-tenancy) - [AWS Multi-Tenant SaaS](https://aws.amazon.com/blogs/apn/building-a-multi-tenant-saas-solution-using-aws-serverless-services/)