2 min read
BackendMulti-Tenancy Architecture
Multi-Tenant SaaS Architektur. Database Isolation, Tenant Context, Row-Level Security und Scaling Strategien.
Multi-TenancySaaS ArchitectureDatabase IsolationRow-Level SecurityTenantB2B

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
// lib/tenant/context.ts
import { AsyncLocalStorage } from 'async_hooks';
export interface TenantContext {
tenantId: string;
tenantSlug: string;
plan: string;
features: string[];
limits: Record<string, number>;
}
const tenantStorage = new AsyncLocalStorage<TenantContext>();
export function getTenantContext(): TenantContext {
const context = tenantStorage.getStore();
if (!context) {
throw new Error('Tenant context not initialized');
}
return context;
}
export function runWithTenant<T>(
tenant: TenantContext,
fn: () => T
): T {
return tenantStorage.run(tenant, fn);
}
// Optional: Get tenant without throwing
export function getTenantContextSafe(): TenantContext | null {
return tenantStorage.getStore() || null;
}// 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<TenantContext | null> {
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
// 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])
}// 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
-- 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;// 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<T>(
sql: string,
params?: unknown[]
): Promise<T[]> {
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<Project>(
'SELECT * FROM projects WHERE status = $1',
['active']
);
// RLS automatically filters by tenant!API Routes with Tenant
// 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
// 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:
- Isolation Strategy: Passend zum Use Case wählen
- Tenant Context: Durchgängig in der Anwendung
- Security: RLS oder Application-Level Filtering
- Provisioning: Automatisierte Tenant-Erstellung
Die richtige Strategie hängt von Kunden und Skalierung ab.
Bildprompts
- "Multi-tenant architecture diagram, database isolation strategies"
- "Tenant provisioning flow, signup to activation"
- "Row-level security visualization, data filtering per tenant"