Menu
Back to Blog
2 min read
Backend

Multi-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

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

StrategyIsolationCostComplexityBest For
**DB per Tenant**HighestHighHighEnterprise
**Schema per Tenant**HighMediumMediumMid-market
**Shared + RLS**MediumLowMediumSMB/Startup
**Hybrid**FlexibleVariableHighMixed 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